From 2b7588a73c7e090f9e228aa70e0f369ca13941bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Jim=C3=A9nez=20Saiz?= Date: Thu, 19 Dec 2024 06:47:02 +0100 Subject: [PATCH] feature/file-flinger (#17) * feat(file-flinger): :sparkles: add new module file-flinger to the repository * docs(general): :memo: update documentation * docs(core): :memo: update docs for new modules * docs(general): :memo: update docs and dependencies * test(general): :test_tube: fix test * ci(ci): :green_heart: fix ci process * refactor(general): :recycle: refactor code * docs(general): :memo: update docs for complete repo * docs(general): :memo: update general docs * docs(general): :memo: update docs * docs(general): :memo: update docs * chore(service-config): :arrow_up: upgrade dependencies and add modify default dependencies * chore(s3): :arrow_up: update s3 dependencies * fix(http-client-provider): :bug: fix schema check in provider * fix(polling): remove redundant max timeout check in RetryManager * chore(general): :arrow_up: update dependencies * ci(ci): :green_heart: fix ci * test(redis-provider): :white_check_mark: update test on redis * ci(general): * test(openc2): :white_check_mark: update test in openc2 * chore(general): :arrow_up: update dependencies * chore(file-flinger): update file-flinger * test(file-flinger): :white_check_mark: add more test for coverage --------- Co-authored-by: Victor --- .config/azure-pipelines.yml | 4 +- .config/envDoc.mjs | 54 +- .config/mdf-publish-artifacts-lerna.yml | 2 +- .config/mdf-test-sonarqube-analysis.yml | 8 +- .config/sonar-project.properties | 59 +- .nvmrc | 2 +- .vscode/settings.json | 5 +- README.md | 6 +- docs/assets/hierarchy.js | 1 + docs/assets/highlight.css | 54 +- docs/assets/icons.js | 2 +- docs/assets/icons.svg | 2 +- docs/assets/main.js | 10 +- .../assets/media/Provider-Class-Hierarchy.png | Bin 0 -> 47022 bytes docs/assets/media/Provider-States-Events.png | Bin 0 -> 23046 bytes docs/assets/media/Provider-States-Methods.png | Bin 0 -> 34090 bytes docs/assets/media/firehose-diagram.svg | 3 + docs/assets/media/logging-capture.png | Bin 0 -> 137218 bytes docs/assets/navigation.js | 2 +- docs/assets/search.js | 2 +- docs/assets/style.css | 2672 +++++++++-------- .../_mdf.js_amqp-provider.Receiver.Port.html | 16 + .../_mdf.js_amqp-provider.Sender.Port.html | 16 + ....js_amqp-provider._internal_.BasePort.html | 60 + ...js_amqp-provider._internal_.Container.html | 4 + ....js_amqp-provider._internal_.Receiver.html | 12 + ...df.js_amqp-provider._internal_.Sender.html | 12 + .../classes/_mdf.js_core.Jobs.JobHandler.html | 83 + .../_mdf.js_core.Layer.Provider.Manager.html | 96 + .../_mdf.js_core.Layer.Provider.Port.html | 131 + docs/classes/_mdf.js_crash.Boom.html | 62 + ...rs.html => _mdf.js_crash.BoomHelpers.html} | 268 +- docs/classes/_mdf.js_crash.Crash.html | 61 + docs/classes/_mdf.js_crash.Multi.html | 71 + .../_mdf.js_crash._internal_.Base.html | 19 + .../_mdf.js_doorkeeper.DoorKeeper.html | 78 + ...mdf.js_elastic-provider.Elastic.Port.html} | 52 +- docs/classes/_mdf.js_faker.Factory.html | 128 + .../_mdf.js_file-flinger.FileFlinger.html | 37 + ...mdf.js_file-flinger._internal_.Engine.html | 32 + ....js_file-flinger._internal_.FileTasks.html | 17 + ...mdf.js_file-flinger._internal_.Keygen.html | 35 + ...ile-flinger._internal_.MetricsHandler.html | 7 + ...df.js_file-flinger._internal_.Watcher.html | 37 + docs/classes/_mdf.js_firehose.Firehose.html | 84 + .../_mdf.js_firehose._internal_.Base-1.html | 24 + .../_mdf.js_firehose._internal_.Base.html | 22 + ...df.js_firehose._internal_.CreditsFlow.html | 24 + .../_mdf.js_firehose._internal_.Engine.html | 41 + .../_mdf.js_firehose._internal_.Flow.html | 24 + .../_mdf.js_firehose._internal_.Jet.html | 26 + ...js_firehose._internal_.MetricsHandler.html | 9 + ....js_firehose._internal_.PlugWrapper-1.html | 19 + ...df.js_firehose._internal_.PlugWrapper.html | 18 + .../_mdf.js_firehose._internal_.Sequence.html | 24 + .../_mdf.js_firehose._internal_.Tap.html | 24 + ...mdf.js_http-client-provider.HTTP.Port.html | 58 + ...mdf.js_http-server-provider.HTTP.Port.html | 58 + ...mdf.js_jsonl-archiver.ArchiverManager.html | 33 + ....js_jsonl-archiver.JSONLArchiver.Port.html | 59 + ...jsonl-archiver._internal_.FileHandler.html | 35 + .../_mdf.js_kafka-provider.Consumer.Port.html | 58 + .../_mdf.js_kafka-provider.Producer.Port.html | 58 + ...js_kafka-provider._internal_.BasePort.html | 61 + ...f.js_kafka-provider._internal_.Client.html | 19 + ...js_kafka-provider._internal_.Consumer.html | 28 + ...js_kafka-provider._internal_.Producer.html | 26 + docs/classes/_mdf.js_logger.DebugLogger.html | 55 + ...Logger.html => _mdf.js_logger.Logger.html} | 78 +- .../classes/_mdf.js_logger.WrapperLogger.html | 56 + docs/classes/_mdf.js_middlewares.Audit.html | 11 + docs/classes/_mdf.js_middlewares.AuthZ.html | 6 + .../_mdf.js_middlewares.BodyParser.html | 18 + ...he.html => _mdf.js_middlewares.Cache.html} | 20 +- ...ors.html => _mdf.js_middlewares.Cors.html} | 8 +- docs/classes/_mdf.js_middlewares.Default.html | 8 + .../_mdf.js_middlewares.ErrorHandler.html | 5 + docs/classes/_mdf.js_middlewares.Logger.html | 4 + ....html => _mdf.js_middlewares.Metrics.html} | 10 +- docs/classes/_mdf.js_middlewares.Multer.html | 69 + docs/classes/_mdf.js_middlewares.NoCache.html | 4 + ...l => _mdf.js_middlewares.RateLimiter.html} | 14 +- .../_mdf.js_middlewares.RequestId.html | 4 + .../classes/_mdf.js_middlewares.Security.html | 5 + ...iddlewares._internal_.CacheRepository.html | 14 + .../_mdf.js_mongo-provider.Mongo.Port.html | 58 + .../_mdf.js_mqtt-provider.MQTT.Port.html | 16 + .../_mdf.js_openc2-core.Accessors.html | 43 + .../classes/_mdf.js_openc2-core.Consumer.html | 100 + .../_mdf.js_openc2-core.ConsumerMap.html | 49 + docs/classes/_mdf.js_openc2-core.Gateway.html | 77 + .../classes/_mdf.js_openc2-core.Producer.html | 93 + ...html => _mdf.js_openc2-core.Registry.html} | 68 +- ...enc2-core._internal_.AdapterWrapper-1.html | 29 + ...openc2-core._internal_.AdapterWrapper.html | 31 + ...f.js_openc2-core._internal_.Component.html | 43 + ....js_openc2-core._internal_.Controller.html | 18 + ..._openc2-core._internal_.HealthWrapper.html | 32 + .../_mdf.js_openc2-core._internal_.Model.html | 11 + ..._mdf.js_openc2-core._internal_.Router.html | 9 + ...mdf.js_openc2-core._internal_.Service.html | 19 + ...2.Adapters.Dummy.DummyConsumerAdapter.html | 30 + ...2.Adapters.Dummy.DummyProducerAdapter.html | 27 + ...2.Adapters.Redis.RedisConsumerAdapter.html | 31 + ...2.Adapters.Redis.RedisProducerAdapter.html | 28 + ...ters.SocketIO.SocketIOConsumerAdapter.html | 31 + ...ters.SocketIO.SocketIOProducerAdapter.html | 28 + .../_mdf.js_openc2.Factory.Consumer.html | 13 + ..._mdf.js_openc2.Factory.GatewayFactory.html | 12 + .../_mdf.js_openc2.Factory.Producer.html | 13 + docs/classes/_mdf.js_openc2.ServiceBus.html | 31 + .../_mdf.js_openc2._internal_.Adapter.html | 9 + ...df.js_openc2._internal_.AddressMapper.html | 26 + ...mdf.js_openc2._internal_.DummyAdapter.html | 25 + ...mdf.js_openc2._internal_.RedisAdapter.html | 26 + ....js_openc2._internal_.SocketIOAdapter.html | 26 + .../_mdf.js_redis-provider.Redis.Port.html | 58 + docs/classes/_mdf.js_s3-provider.S3.Port.html | 58 + ...f.js_service-registry.ServiceRegistry.html | 64 + ...vice-registry._internal_.Aggregator-1.html | 27 + ...vice-registry._internal_.Aggregator-2.html | 58 + ...ervice-registry._internal_.Aggregator.html | 33 + ...ce-registry._internal_.ControlManager.html | 29 + ...vice-registry._internal_.Controller-1.html | 10 + ...vice-registry._internal_.Controller-2.html | 11 + ...vice-registry._internal_.Controller-3.html | 10 + ...ervice-registry._internal_.Controller.html | 15 + ...vice-registry._internal_.HealthFacade.html | 55 + ...ice-registry._internal_.MetricsFacade.html | 57 + ...s_service-registry._internal_.Model-1.html | 7 + ...s_service-registry._internal_.Model-2.html | 7 + ...s_service-registry._internal_.Model-3.html | 7 + ....js_service-registry._internal_.Model.html | 11 + ...ice-registry._internal_.Observability.html | 57 + ...ry._internal_.ObservabilityAppManager.html | 22 + ...js_service-registry._internal_.Port-1.html | 10 + ...f.js_service-registry._internal_.Port.html | 12 + ...ce-registry._internal_.RegisterFacade.html | 48 + ..._service-registry._internal_.Router-1.html | 8 + ..._service-registry._internal_.Router-2.html | 9 + ..._service-registry._internal_.Router-3.html | 8 + ...js_service-registry._internal_.Router.html | 8 + ...service-registry._internal_.Service-1.html | 7 + ...service-registry._internal_.Service-2.html | 7 + ...service-registry._internal_.Service-3.html | 7 + ...s_service-registry._internal_.Service.html | 11 + ...y._internal_.SettingsManagerAccessors.html | 70 + ...gistry._internal_.SettingsManagerBase.html | 30 + ...service-registry._internal_.Validator.html | 8 + ..._service-setup-provider.ConfigManager.html | 34 + ....js_service-setup-provider.Setup.Port.html | 60 + ...t-client-provider.SocketIOClient.Port.html | 58 + ...t-server-provider.SocketIOServer.Port.html | 58 + docs/classes/_mdf.js_tasks.Group.html | 60 + docs/classes/_mdf.js_tasks.Limiter.html | 131 + .../_mdf.js_tasks.PollingExecutor.html | 17 + docs/classes/_mdf.js_tasks.Scheduler.html | 71 + docs/classes/_mdf.js_tasks.Sequence.html | 58 + docs/classes/_mdf.js_tasks.Single.html | 62 + docs/classes/_mdf.js_tasks.TaskHandler.html | 57 + ..._tasks._internal_.LimiterStateHandler.html | 104 + ...df.js_tasks._internal_.PollingManager.html | 19 + ...asks._internal_.PollingMetricsHandler.html | 22 + .../_mdf.js_tasks._internal_.Queue.html | 38 + ..._mdf.js_tasks._internal_.RetryManager.html | 14 + .../classes/_mdf_js_core.Jobs.JobHandler.html | 83 - .../_mdf_js_core.Layer.Provider.Manager.html | 96 - .../_mdf_js_core.Layer.Provider.Port.html | 131 - docs/classes/_mdf_js_crash.Boom.html | 62 - docs/classes/_mdf_js_crash.Crash.html | 61 - docs/classes/_mdf_js_crash.Multi.html | 71 - .../_mdf_js_doorkeeper.DoorKeeper.html | 78 - docs/classes/_mdf_js_faker.Factory.html | 128 - docs/classes/_mdf_js_firehose.Firehose.html | 77 - ...mdf_js_jsonl_archiver.ArchiverManager.html | 33 - docs/classes/_mdf_js_logger.DebugLogger.html | 55 - docs/classes/_mdf_js_middlewares.Audit.html | 11 - docs/classes/_mdf_js_middlewares.AuthZ.html | 6 - .../_mdf_js_middlewares.BodyParser.html | 18 - docs/classes/_mdf_js_middlewares.Default.html | 8 - .../_mdf_js_middlewares.ErrorHandler.html | 5 - docs/classes/_mdf_js_middlewares.Logger.html | 4 - docs/classes/_mdf_js_middlewares.Multer.html | 69 - docs/classes/_mdf_js_middlewares.NoCache.html | 4 - .../_mdf_js_middlewares.RequestId.html | 4 - .../classes/_mdf_js_middlewares.Security.html | 5 - ...2.Adapters.Dummy.DummyConsumerAdapter.html | 24 - ...2.Adapters.Dummy.DummyProducerAdapter.html | 21 - ...2.Adapters.Redis.RedisConsumerAdapter.html | 25 - ...2.Adapters.Redis.RedisProducerAdapter.html | 22 - ...ters.SocketIO.SocketIOConsumerAdapter.html | 25 - ...ters.SocketIO.SocketIOProducerAdapter.html | 22 - .../_mdf_js_openc2.Factory.Consumer.html | 13 - ..._mdf_js_openc2.Factory.GatewayFactory.html | 12 - .../_mdf_js_openc2.Factory.Producer.html | 13 - docs/classes/_mdf_js_openc2.ServiceBus.html | 31 - .../_mdf_js_openc2_core.Accessors.html | 32 - .../classes/_mdf_js_openc2_core.Consumer.html | 91 - docs/classes/_mdf_js_openc2_core.Gateway.html | 77 - .../classes/_mdf_js_openc2_core.Producer.html | 84 - ...f_js_service_registry.ServiceRegistry.html | 64 - ..._service_setup_provider.ConfigManager.html | 34 - docs/classes/_mdf_js_tasks.Group.html | 50 - docs/classes/_mdf_js_tasks.Limiter.html | 121 - .../_mdf_js_tasks.PollingExecutor.html | 17 - docs/classes/_mdf_js_tasks.Scheduler.html | 71 - docs/classes/_mdf_js_tasks.Sequence.html | 48 - docs/classes/_mdf_js_tasks.Single.html | 52 - docs/classes/_mdf_js_tasks.TaskHandler.html | 47 - ...S.html => _mdf.js_core.Health.STATUS.html} | 10 +- ...tus.html => _mdf.js_core.Jobs.Status.html} | 20 +- ...s_core.Layer.Provider.ProviderStatus.html} | 10 +- .../_mdf.js_file-flinger.ErrorStrategy.html | 8 + ...s_file-flinger.PostProcessingStrategy.html | 8 + ...> _mdf.js_openc2-core.Control.Action.html} | 84 +- ..._mdf.js_openc2-core.Control.Features.html} | 20 +- ...df.js_openc2-core.Control.L4Protocol.html} | 20 +- ...f.js_openc2-core.Control.MessageType.html} | 10 +- ....js_openc2-core.Control.ResponseType.html} | 20 +- ...df.js_openc2-core.Control.StatusCode.html} | 40 +- ...e.html => _mdf.js_tasks.LimiterState.html} | 24 +- ...html => _mdf.js_tasks.RETRY_STRATEGY.html} | 20 +- ...ATEGY.html => _mdf.js_tasks.STRATEGY.html} | 20 +- ...ATE.html => _mdf.js_tasks.TASK_STATE.html} | 24 +- .../_mdf.js_core.Health.overallStatus.html | 2 + ...Layer.Provider.ProviderFactoryCreator.html | 7 + docs/functions/_mdf.js_logger.SetContext.html | 5 + docs/functions/_mdf.js_utils.coerce.html | 20 + docs/functions/_mdf.js_utils.deCycle.html | 5 + .../functions/_mdf.js_utils.escapeRegExp.html | 3 + .../_mdf.js_utils.findNodeModule.html | 5 + docs/functions/_mdf.js_utils.formatEnv.html | 16 + docs/functions/_mdf.js_utils.loadFile.html | 3 + docs/functions/_mdf.js_utils.prettyMS.html | 3 + docs/functions/_mdf.js_utils.retroCycle.html | 4 + docs/functions/_mdf.js_utils.retry.html | 5 + docs/functions/_mdf.js_utils.retryBind.html | 6 + docs/functions/_mdf.js_utils.wrapOnRetry.html | 8 + .../_mdf_js_core.Health.overallStatus.html | 2 - ...Layer.Provider.ProviderFactoryCreator.html | 7 - docs/functions/_mdf_js_logger.SetContext.html | 5 - docs/functions/_mdf_js_utils.coerce.html | 20 - docs/functions/_mdf_js_utils.deCycle.html | 5 - .../functions/_mdf_js_utils.escapeRegExp.html | 3 - .../_mdf_js_utils.findNodeModule.html | 5 - docs/functions/_mdf_js_utils.formatEnv.html | 16 - docs/functions/_mdf_js_utils.loadFile.html | 3 - docs/functions/_mdf_js_utils.prettyMS.html | 3 - docs/functions/_mdf_js_utils.retroCycle.html | 4 - docs/functions/_mdf_js_utils.retry.html | 5 - docs/functions/_mdf_js_utils.retryBind.html | 6 - docs/functions/_mdf_js_utils.wrapOnRetry.html | 8 - docs/hierarchy.html | 2 +- docs/index.html | 36 +- ...ck.html => _mdf.js_core.Health.Check.html} | 38 +- .../_mdf.js_core.Jobs.DefaultOptions.html | 6 + .../_mdf.js_core.Jobs.JobObject.html | 16 + .../_mdf.js_core.Jobs.JobRequest.html | 9 + .../_mdf.js_core.Jobs.NoMoreHeaders.html | 2 + .../_mdf.js_core.Jobs.NoMoreOptions.html | 2 + docs/interfaces/_mdf.js_core.Jobs.Result.html | 22 + .../_mdf.js_core.Jobs.Strategy.html | 7 + .../_mdf.js_core.Layer.App.Component.html | 12 + .../_mdf.js_core.Layer.App.Health.html | 101 + .../_mdf.js_core.Layer.App.Metadata.html | 63 + .../_mdf.js_core.Layer.App.Resource.html | 28 + .../_mdf.js_core.Layer.App.Service.html | 37 + .../_mdf.js_core.Layer.Provider.Factory.html | 5 + ...s_core.Layer.Provider.FactoryOptions.html} | 18 +- ...r.Provider.PortConfigValidationStruct.html | 9 + ...s_core.Layer.Provider.ProviderOptions.html | 25 + .../_mdf.js_core._internal_.State.html | 11 + ...Error.html => _mdf.js_crash.APIError.html} | 26 +- docs/interfaces/_mdf.js_crash.APISource.html | 6 + .../interfaces/_mdf.js_crash.BoomOptions.html | 13 + ...ontext.html => _mdf.js_crash.Context.html} | 10 +- .../interfaces/_mdf.js_crash.CrashObject.html | 16 + .../_mdf.js_crash.CrashOptions.html | 12 + .../interfaces/_mdf.js_crash.MultiObject.html | 16 + .../_mdf.js_crash.MultiOptions.html | 12 + .../_mdf.js_crash.ValidationError.html | 12 + .../_mdf.js_crash.ValidationErrorItem.html | 6 + .../_mdf.js_crash._internal_.BaseObject.html | 16 + .../_mdf.js_crash._internal_.BaseOptions.html | 9 + .../_mdf.js_doorkeeper.DoorkeeperOptions.html | 5 + ....html => _mdf.js_faker.DefaultObject.html} | 4 +- .../_mdf.js_faker.DefaultOptions.html | 3 + .../_mdf.js_faker._internal_.Entry.html | 4 + ...df.js_file-flinger.FileFlingerOptions.html | 49 + .../_mdf.js_file-flinger.Pusher.html | 6 + ...file-flinger._internal_.EngineOptions.html | 20 + ...s_file-flinger._internal_.ErroredFile.html | 8 + ...e-flinger._internal_.FileTasksOptions.html | 14 + ...file-flinger._internal_.KeygenOptions.html | 27 + ...ile-flinger._internal_.WatcherOptions.html | 12 + .../_mdf.js_firehose.FirehoseOptions.html | 19 + .../_mdf.js_firehose.Plugs.Sink.Jet.html | 19 + .../_mdf.js_firehose.Plugs.Sink.Tap.html | 15 + ....js_firehose.Plugs.Source.CreditsFlow.html | 24 + .../_mdf.js_firehose.Plugs.Source.Flow.html | 25 + ...mdf.js_firehose.Plugs.Source.Sequence.html | 24 + .../_mdf.js_firehose.PostConsumeOptions.html | 8 + .../_mdf.js_firehose._internal_.Base-2.html | 19 + .../_mdf.js_firehose._internal_.Base-3.html | 24 + ....js_firehose._internal_.EngineOptions.html | 9 + ...df.js_firehose._internal_.SinkOptions.html | 7 + ....js_firehose._internal_.SourceOptions.html | 12 + ...firehose._internal_.WrappableSinkPlug.html | 19 + ...rehose._internal_.WrappableSourcePlug.html | 46 + ...f.js_http-client-provider.HTTP.Config.html | 4 + ...f.js_http-server-provider.HTTP.Config.html | 8 + .../_mdf.js_jsonl-archiver.AppendResult.html | 13 + ..._mdf.js_jsonl-archiver.ArchiveOptions.html | 129 + .../_mdf.js_jsonl-archiver.FileStats.html | 30 + ...rchiver._internal_.FileHandlerOptions.html | 131 + ...mdf.js_kafka-provider.Consumer.Config.html | 7 + ...df.js_kafka-provider.Producer.Config.html} | 8 +- ..._kafka-provider._internal_.BaseConfig.html | 5 + ...mdf.js_logger.ConsoleTransportConfig.html} | 8 +- ...> _mdf.js_logger.FileTransportConfig.html} | 22 +- ....html => _mdf.js_logger.LoggerConfig.html} | 10 +- .../_mdf.js_logger.LoggerInstance.html | 52 + ...l => _mdf.js_middlewares.AuditConfig.html} | 24 +- .../_mdf.js_middlewares.CacheConfig.html | 19 + .../_mdf.js_middlewares.CorsConfig.html | 14 + .../_mdf.js_middlewares.RateLimitConfig.html | 6 + .../_mdf.js_middlewares.RateLimitEntry.html | 4 + ...f.js_mongo-provider.Mongo.Collections.html | 1 + ... _mdf.js_mongo-provider.Mongo.Config.html} | 8 +- .../_mdf.js_mqtt-provider.MQTT.Config.html | 3 + .../_mdf.js_openc2-core.CommandJobHeader.html | 3 + .../_mdf.js_openc2-core.ConsumerAdapter.html | 20 + .../_mdf.js_openc2-core.ConsumerOptions.html | 21 + .../_mdf.js_openc2-core.Control.Actuator.html | 2 + ...mdf.js_openc2-core.Control.Arguments.html} | 18 +- ..._mdf.js_openc2-core.Control.Artifact.html} | 16 +- ... _mdf.js_openc2-core.Control.Command.html} | 24 +- ...js_openc2-core.Control.CommandMessage.html | 19 + ...> _mdf.js_openc2-core.Control.Device.html} | 14 +- ... => _mdf.js_openc2-core.Control.File.html} | 12 +- ...> _mdf.js_openc2-core.Control.Hashes.html} | 10 +- ...s_openc2-core.Control.IPv4Connection.html} | 24 +- ...s_openc2-core.Control.IPv6Connection.html} | 24 +- ... _mdf.js_openc2-core.Control.Payload.html} | 8 +- ... _mdf.js_openc2-core.Control.Process.html} | 22 +- ..._mdf.js_openc2-core.Control.Response.html} | 16 +- ...s_openc2-core.Control.ResponseMessage.html | 21 + ... _mdf.js_openc2-core.Control.Results.html} | 18 +- ...> _mdf.js_openc2-core.Control.Target.html} | 70 +- .../_mdf.js_openc2-core.GatewayOptions.html | 32 + .../_mdf.js_openc2-core.ProducerAdapter.html | 17 + .../_mdf.js_openc2-core.ProducerOptions.html | 20 + ...js_openc2-core._internal_.BaseMessage.html | 15 + ...enc2-core._internal_.ComponentAdapter.html | 14 + ...enc2-core._internal_.ComponentOptions.html | 13 + ..._openc2-core._internal_.GatewayTimers.html | 6 + .../_mdf.js_openc2.AdapterOptions.html | 10 + .../_mdf.js_openc2.ServiceBusOptions.html | 5 + ...redis-provider._internal_.MemoryStats.html | 105 + ...redis-provider._internal_.ServerStats.html | 52 + ...f.js_redis-provider._internal_.Status.html | 5 + ....js_service-registry.BootstrapOptions.html | 74 + ..._service-registry.ExtendedCrashObject.html | 5 + ..._service-registry.ExtendedMultiObject.html | 5 + ...-registry.ObservabilityServiceOptions.html | 18 + ...rvice-registry.ServiceRegistryOptions.html | 43 + ...vice-registry.ServiceRegistrySettings.html | 43 + ...df.js_service-registry.ServiceSetting.html | 38 + ...stry._internal_.HealthRegistryOptions.html | 9 + ...try._internal_.MetricsRegistryOptions.html | 15 + ...istry._internal_.ObservabilityOptions.html | 7 + ...e-registry._internal_.RegistryOptions.html | 21 + ..._service-registry._internal_.Response.html | 5 + ...s_service-setup-provider.Setup.Config.html | 51 + ...client-provider.SocketIOClient.Config.html | 3 + ...er.SocketIOServer.BasicAuthentication.html | 5 + ...erver-provider.SocketIOServer.Config.html} | 14 +- ...ovider.SocketIOServer.ConnectionError.html | 12 + ...ider.SocketIOServer.InstrumentOptions.html | 21 + ...f.js_tasks.ConsolidatedLimiterOptions.html | 54 + .../_mdf.js_tasks.GroupTaskBaseConfig.html | 6 + .../_mdf.js_tasks.LimiterOptions.html | 58 + docs/interfaces/_mdf.js_tasks.MetaData.html | 29 + .../_mdf.js_tasks.PollingManagerOptions.html | 21 + .../_mdf.js_tasks.PollingStats.html | 28 + .../_mdf.js_tasks.QueueOptions.html | 36 + .../_mdf.js_tasks.ResourceConfigEntry.html | 6 + .../_mdf.js_tasks.ResourcesConfigObject.html | 4 + .../_mdf.js_tasks.SchedulerOptions.html | 18 + ...tml => _mdf.js_tasks.SequencePattern.html} | 20 +- .../_mdf.js_tasks.SequenceTaskBaseConfig.html | 6 + .../_mdf.js_tasks.SingleTaskBaseConfig.html | 8 + .../interfaces/_mdf.js_tasks.TaskOptions.html | 32 + ...df.js_tasks.WellIdentifiedTaskOptions.html | 29 + ...s._internal_.ConsolidatedQueueOptions.html | 36 + .../_mdf.js_utils.ReadEnvOptions.html | 8 + .../_mdf.js_utils.RetryOptions.html | 18 + .../_mdf_js_core.Jobs.DefaultOptions.html | 6 - .../_mdf_js_core.Jobs.JobObject.html | 16 - .../_mdf_js_core.Jobs.JobRequest.html | 9 - .../_mdf_js_core.Jobs.NoMoreHeaders.html | 2 - .../_mdf_js_core.Jobs.NoMoreOptions.html | 2 - docs/interfaces/_mdf_js_core.Jobs.Result.html | 22 - .../_mdf_js_core.Jobs.Strategy.html | 7 - .../_mdf_js_core.Layer.App.Component.html | 12 - .../_mdf_js_core.Layer.App.Health.html | 101 - .../_mdf_js_core.Layer.App.Metadata.html | 63 - .../_mdf_js_core.Layer.App.Resource.html | 28 - .../_mdf_js_core.Layer.App.Service.html | 37 - .../_mdf_js_core.Layer.Provider.Factory.html | 5 - ...r.Provider.PortConfigValidationStruct.html | 9 - ...s_core.Layer.Provider.ProviderOptions.html | 25 - docs/interfaces/_mdf_js_crash.APISource.html | 6 - .../interfaces/_mdf_js_crash.BoomOptions.html | 12 - .../interfaces/_mdf_js_crash.CrashObject.html | 16 - .../_mdf_js_crash.CrashOptions.html | 11 - .../interfaces/_mdf_js_crash.MultiObject.html | 16 - .../_mdf_js_crash.MultiOptions.html | 11 - .../_mdf_js_crash.ValidationError.html | 12 - .../_mdf_js_crash.ValidationErrorItem.html | 6 - .../_mdf_js_doorkeeper.DoorkeeperOptions.html | 5 - .../_mdf_js_faker.DefaultOptions.html | 3 - .../_mdf_js_firehose.FirehoseOptions.html | 19 - .../_mdf_js_firehose.Plugs.Sink.Jet.html | 16 - ..._js_firehose.Plugs.Source.CreditsFlow.html | 21 - .../_mdf_js_firehose.Plugs.Source.Flow.html | 22 - ...mdf_js_firehose.Plugs.Source.Sequence.html | 21 - .../_mdf_js_firehose.PostConsumeOptions.html | 8 - ...f_js_http_client_provider.HTTP.Config.html | 4 - ...f_js_http_server_provider.HTTP.Config.html | 4 - ..._mdf_js_jsonl_archiver.ArchiveOptions.html | 129 - .../_mdf_js_jsonl_archiver.FileStats.html | 30 - ...mdf_js_kafka_provider.Consumer.Config.html | 7 - .../_mdf_js_logger.LoggerInstance.html | 52 - .../_mdf_js_middlewares.CacheConfig.html | 19 - .../_mdf_js_middlewares.CorsConfig.html | 14 - .../_mdf_js_middlewares.RateLimitConfig.html | 3 - ...f_js_mongo_provider.Mongo.Collections.html | 1 - .../_mdf_js_mqtt_provider.MQTT.Config.html | 3 - .../_mdf_js_openc2.ServiceBusOptions.html | 5 - .../_mdf_js_openc2_core.ConsumerAdapter.html | 14 - .../_mdf_js_openc2_core.ConsumerOptions.html | 21 - .../_mdf_js_openc2_core.Control.Actuator.html | 2 - ...js_openc2_core.Control.CommandMessage.html | 19 - ...s_openc2_core.Control.ResponseMessage.html | 21 - .../_mdf_js_openc2_core.GatewayOptions.html | 32 - .../_mdf_js_openc2_core.ProducerAdapter.html | 11 - .../_mdf_js_openc2_core.ProducerOptions.html | 20 - ..._registry.ObservabilityServiceOptions.html | 18 - ...rvice_registry.ServiceRegistryOptions.html | 43 - ...vice_registry.ServiceRegistrySettings.html | 43 - ...df_js_service_registry.ServiceSetting.html | 38 - ...s_service_setup_provider.Setup.Config.html | 51 - ...client_provider.SocketIOClient.Config.html | 3 - .../_mdf_js_tasks.GroupTaskBaseConfig.html | 6 - .../_mdf_js_tasks.LimiterOptions.html | 58 - docs/interfaces/_mdf_js_tasks.MetaData.html | 29 - .../_mdf_js_tasks.PollingManagerOptions.html | 21 - .../_mdf_js_tasks.PollingStats.html | 28 - .../_mdf_js_tasks.QueueOptions.html | 36 - .../_mdf_js_tasks.ResourceConfigEntry.html | 6 - .../_mdf_js_tasks.ResourcesConfigObject.html | 4 - .../_mdf_js_tasks.SchedulerOptions.html | 18 - .../_mdf_js_tasks.SequenceTaskBaseConfig.html | 6 - .../_mdf_js_tasks.SingleTaskBaseConfig.html | 8 - .../interfaces/_mdf_js_tasks.TaskOptions.html | 32 - ...df_js_tasks.WellIdentifiedTaskOptions.html | 29 - .../_mdf_js_utils.RetryOptions.html | 18 - docs/media/firehose-diagram.svg | 3 + docs/media/logging-capture-1.png | Bin 0 -> 137218 bytes docs/modules.html | 1 + .../_mdf.js_amqp-provider.Receiver.html | 4 + .../modules/_mdf.js_amqp-provider.Sender.html | 1 + .../_mdf.js_amqp-provider._internal_.html | 1 + docs/modules/_mdf.js_amqp-provider.html | 162 + docs/modules/_mdf.js_core.Health.html | 4 + docs/modules/_mdf.js_core.Jobs.html | 1 + docs/modules/_mdf.js_core.Layer.App.html | 1 + docs/modules/_mdf.js_core.Layer.Provider.html | 1 + docs/modules/_mdf.js_core.Layer.html | 1 + docs/modules/_mdf.js_core._internal_.html | 1 + docs/modules/_mdf.js_core.html | 487 +++ docs/modules/_mdf.js_crash._internal_.html | 1 + docs/modules/_mdf.js_crash.html | 116 + docs/modules/_mdf.js_doorkeeper.html | 51 + .../_mdf.js_elastic-provider.Elastic.html | 4 + .../_mdf.js_elastic-provider._internal_.html | 1 + docs/modules/_mdf.js_elastic-provider.html | 100 + docs/modules/_mdf.js_faker._internal_.html | 1 + docs/modules/_mdf.js_faker.html | 102 + .../_mdf.js_file-flinger._internal_.html | 1 + docs/modules/_mdf.js_file-flinger.html | 300 ++ docs/modules/_mdf.js_firehose.Plugs.Sink.html | 4 + .../_mdf.js_firehose.Plugs.Source.html | 1 + docs/modules/_mdf.js_firehose.Plugs.html | 1 + docs/modules/_mdf.js_firehose._internal_.html | 1 + docs/modules/_mdf.js_firehose.html | 279 ++ .../_mdf.js_http-client-provider.HTTP.html | 4 + .../modules/_mdf.js_http-client-provider.html | 92 + .../_mdf.js_http-server-provider.HTTP.html | 4 + .../modules/_mdf.js_http-server-provider.html | 80 + .../_mdf.js_jsonl-archiver.JSONLArchiver.html | 1 + .../_mdf.js_jsonl-archiver._internal_.html | 1 + docs/modules/_mdf.js_jsonl-archiver.html | 111 + .../_mdf.js_kafka-provider.Consumer.html | 4 + .../_mdf.js_kafka-provider.Producer.html | 1 + .../_mdf.js_kafka-provider._internal_.html | 1 + docs/modules/_mdf.js_kafka-provider.html | 111 + docs/modules/_mdf.js_logger.html | 150 + .../_mdf.js_middlewares._internal_.html | 1 + docs/modules/_mdf.js_middlewares.html | 29 + .../modules/_mdf.js_mongo-provider.Mongo.html | 4 + docs/modules/_mdf.js_mongo-provider.html | 80 + docs/modules/_mdf.js_mqtt-provider.MQTT.html | 4 + docs/modules/_mdf.js_mqtt-provider.html | 70 + docs/modules/_mdf.js_openc2-core.Control.html | 1 + .../_mdf.js_openc2-core._internal_.html | 1 + docs/modules/_mdf.js_openc2-core.html | 29 + .../_mdf.js_openc2.Adapters.Dummy.html | 4 + .../_mdf.js_openc2.Adapters.Redis.html | 1 + .../_mdf.js_openc2.Adapters.SocketIO.html | 1 + docs/modules/_mdf.js_openc2.Adapters.html | 1 + docs/modules/_mdf.js_openc2.Factory.html | 1 + docs/modules/_mdf.js_openc2._internal_.html | 1 + docs/modules/_mdf.js_openc2.html | 29 + .../modules/_mdf.js_redis-provider.Redis.html | 4 + .../_mdf.js_redis-provider._internal_.html | 1 + docs/modules/_mdf.js_redis-provider.html | 69 + docs/modules/_mdf.js_s3-provider.S3.html | 4 + docs/modules/_mdf.js_s3-provider.html | 50 + .../_mdf.js_service-registry._internal_.html | 1 + docs/modules/_mdf.js_service-registry.html | 336 +++ .../_mdf.js_service-setup-provider.Setup.html | 1 + ....js_service-setup-provider._internal_.html | 1 + .../_mdf.js_service-setup-provider.html | 139 + ...socket-client-provider.SocketIOClient.html | 4 + .../_mdf.js_socket-client-provider.html | 57 + ...socket-server-provider.SocketIOServer.html | 4 + .../_mdf.js_socket-server-provider.html | 57 + docs/modules/_mdf.js_tasks._internal_.html | 1 + docs/modules/_mdf.js_tasks.html | 315 ++ docs/modules/_mdf.js_utils.html | 176 ++ .../_mdf_js_amqp_provider.Receiver.html | 6 - .../modules/_mdf_js_amqp_provider.Sender.html | 3 - docs/modules/_mdf_js_amqp_provider.html | 163 - docs/modules/_mdf_js_core.Health.html | 13 - docs/modules/_mdf_js_core.Jobs.html | 15 - docs/modules/_mdf_js_core.Layer.App.html | 6 - docs/modules/_mdf_js_core.Layer.Provider.html | 11 - docs/modules/_mdf_js_core.Layer.html | 4 - docs/modules/_mdf_js_core.html | 489 --- docs/modules/_mdf_js_crash.html | 133 - docs/modules/_mdf_js_doorkeeper.html | 55 - .../_mdf_js_elastic_provider.Elastic.html | 24 - docs/modules/_mdf_js_elastic_provider.html | 100 - docs/modules/_mdf_js_faker.html | 107 - docs/modules/_mdf_js_firehose.Plugs.Sink.html | 7 - .../_mdf_js_firehose.Plugs.Source.html | 5 - docs/modules/_mdf_js_firehose.Plugs.html | 3 - docs/modules/_mdf_js_firehose.html | 33 - .../_mdf_js_http_client_provider.HTTP.html | 7 - .../modules/_mdf_js_http_client_provider.html | 65 - .../_mdf_js_http_server_provider.HTTP.html | 7 - .../modules/_mdf_js_http_server_provider.html | 53 - .../_mdf_js_jsonl_archiver.JSONLArchiver.html | 5 - docs/modules/_mdf_js_jsonl_archiver.html | 114 - .../_mdf_js_kafka_provider.Consumer.html | 7 - .../_mdf_js_kafka_provider.Producer.html | 4 - docs/modules/_mdf_js_kafka_provider.html | 113 - docs/modules/_mdf_js_logger.html | 37 - docs/modules/_mdf_js_middlewares.html | 52 - .../modules/_mdf_js_mongo_provider.Mongo.html | 8 - docs/modules/_mdf_js_mongo_provider.html | 79 - docs/modules/_mdf_js_mqtt_provider.MQTT.html | 7 - docs/modules/_mdf_js_mqtt_provider.html | 69 - .../_mdf_js_openc2.Adapters.Dummy.html | 7 - .../_mdf_js_openc2.Adapters.Redis.html | 4 - .../_mdf_js_openc2.Adapters.SocketIO.html | 4 - docs/modules/_mdf_js_openc2.Adapters.html | 4 - docs/modules/_mdf_js_openc2.Factory.html | 4 - docs/modules/_mdf_js_openc2.html | 32 - docs/modules/_mdf_js_openc2_core.Control.html | 29 - docs/modules/_mdf_js_openc2_core.html | 45 - .../modules/_mdf_js_redis_provider.Redis.html | 7 - docs/modules/_mdf_js_redis_provider.html | 68 - docs/modules/_mdf_js_s3_provider.S3.html | 6 - docs/modules/_mdf_js_s3_provider.html | 50 - docs/modules/_mdf_js_service_registry.html | 345 --- .../_mdf_js_service_setup_provider.Setup.html | 5 - .../_mdf_js_service_setup_provider.html | 140 - ...socket_client_provider.SocketIOClient.html | 7 - .../_mdf_js_socket_client_provider.html | 56 - ...socket_server_provider.SocketIOServer.html | 7 - .../_mdf_js_socket_server_provider.html | 56 - docs/modules/_mdf_js_tasks.html | 349 --- docs/modules/_mdf_js_utils.html | 192 -- ...df.js_amqp-provider.Receiver.Provider.html | 1 + ..._mdf.js_amqp-provider.Sender.Provider.html | 1 + .../types/_mdf.js_core.Health.CheckEntry.html | 2 + docs/types/_mdf.js_core.Health.Checks.html | 31 + .../_mdf.js_core.Health.ComponentName.html | 2 + .../_mdf.js_core.Health.MeasurementName.html | 2 + docs/types/_mdf.js_core.Health.Status-1.html | 2 + docs/types/_mdf.js_core.Jobs.AnyHeaders.html | 2 + docs/types/_mdf.js_core.Jobs.AnyOptions.html | 2 + .../_mdf.js_core.Jobs.DoneEventHandler.html | 7 + docs/types/_mdf.js_core.Jobs.Headers.html | 2 + docs/types/_mdf.js_core.Jobs.Options.html | 2 + docs/types/_mdf.js_core.Layer.Observable.html | 2 + ....js_core.Layer.Provider.ProviderState.html | 2 + docs/types/_mdf.js_crash.Cause.html | 2 + docs/types/_mdf.js_crash.ContextLink.html | 2 + docs/types/_mdf.js_crash.Links.html | 2 + docs/types/_mdf.js_crash.SimpleLink.html | 2 + .../_mdf.js_doorkeeper.ResultCallback.html | 2 + .../_mdf.js_doorkeeper.SchemaSelector.html | 1 + .../_mdf.js_doorkeeper.ValidatedOutput.html | 1 + ....js_elastic-provider.Elastic.Provider.html | 1 + ...js_elastic-provider._internal_.Status.html | 16 + docs/types/_mdf.js_faker.Builder.html | 2 + docs/types/_mdf.js_faker.DefaultValue.html | 2 + docs/types/_mdf.js_faker.Dependencies.html | 2 + ....js_faker._internal_.GeneratorOptions.html | 2 + ...nger._internal_.InternalKeygenOptions.html | 2 + ...ger._internal_.InternalWatcherOptions.html | 2 + ...le-flinger._internal_.MetricInstances.html | 5 + .../_mdf.js_firehose.JobEventHandler.html | 2 + .../_mdf.js_firehose.Plugs.Sink.Any.html | 1 + .../_mdf.js_firehose.Plugs.Source.Any.html | 1 + ...s_firehose._internal_.MetricInstances.html | 7 + ...js_firehose._internal_.OpenJobHandler.html | 2 + ....js_firehose._internal_.OpenJobObject.html | 2 + ...js_firehose._internal_.OpenJobRequest.html | 2 + ....js_firehose._internal_.OpenStrategy.html} | 4 +- .../_mdf.js_firehose._internal_.Sinks.html | 1 + .../_mdf.js_firehose._internal_.Sources.html | 1 + ...js_http-client-provider.HTTP.Provider.html | 1 + ...js_http-server-provider.HTTP.Provider.html | 1 + ...s_jsonl-archiver.JSONLArchiver.Config.html | 1 + ...jsonl-archiver.JSONLArchiver.Provider.html | 1 + ...f.js_kafka-provider.Consumer.Provider.html | 1 + ...f.js_kafka-provider.Producer.Provider.html | 1 + ...afka-provider._internal_.SystemStatus.html | 1 + ..._mdf.js_logger.FluentdTransportConfig.html | 4 + docs/types/_mdf.js_logger.LogLevel.html | 1 + docs/types/_mdf.js_logger.LoggerFunction.html | 1 + ...js_middlewares.AfterRoutesMiddlewares.html | 1 + .../_mdf.js_middlewares.AuditCategory.html | 4 + .../_mdf.js_middlewares.AuthZOptions.html | 6 + ...s_middlewares.BeforeRoutesMiddlewares.html | 1 + ...f.js_middlewares.EndpointsMiddlewares.html | 1 + .../_mdf.js_middlewares.Middlewares.html | 1 + ....js_middlewares._internal_.CacheEntry.html | 6 + ...iddlewares._internal_.MetricInstances.html | 14 + ...es._internal_.OutgoingHttpHeaderValue.html | 2 + ..._mdf.js_mongo-provider.Mongo.Provider.html | 1 + .../_mdf.js_mqtt-provider.MQTT.Provider.html | 1 + .../_mdf.js_openc2-core.CommandJobDone.html | 1 + ..._mdf.js_openc2-core.CommandJobHandler.html | 1 + ...openc2-core.Control.ActionTargetPairs.html | 2 + ...mdf.js_openc2-core.Control.ActionType.html | 1 + ...re.Control.AllowedResultPropertyTypes.html | 2 + .../_mdf.js_openc2-core.Control.Message.html | 1 + ..._mdf.js_openc2-core.Control.Namespace.html | 2 + .../_mdf.js_openc2-core.OnCommandHandler.html | 1 + docs/types/_mdf.js_openc2-core.Resolver.html | 1 + .../_mdf.js_openc2-core.ResolverEntry.html | 1 + .../_mdf.js_openc2-core.ResolverMap.html | 1 + ...penc2-core._internal_.CommandResponse.html | 1 + ...ore._internal_.CommandResponseHandler.html | 1 + .../_mdf.js_openc2.Adapters.Dummy.Config.html | 1 + .../_mdf.js_openc2.Adapters.Redis.Config.html | 1 + ...df.js_openc2.Adapters.SocketIO.Config.html | 1 + .../_mdf.js_openc2.RedisClientOptions.html | 2 + .../_mdf.js_openc2.SocketIOClientOptions.html | 2 + .../_mdf.js_openc2.SocketIOServerOptions.html | 2 + .../_mdf.js_redis-provider.Redis.Config.html | 1 + ..._mdf.js_redis-provider.Redis.Provider.html | 1 + ...ovider._internal_.ProcessesSupervised.html | 4 + .../_mdf.js_s3-provider.S3.Provider.html | 1 + ...rvice-registry.ConsumerAdapterOptions.html | 8 + ...mdf.js_service-registry.CustomSetting.html | 2 + ...df.js_service-registry.CustomSettings.html | 2 + .../_mdf.js_service-registry.ErrorRecord.html | 2 + ...e-registry._internal_.HandleableError.html | 1 + ...service-setup-provider.Setup.Provider.html | 1 + ...e-setup-provider._internal_.FileEntry.html | 1 + ...ient-provider.SocketIOClient.Provider.html | 1 + ...rver-provider.SocketIOServer.Provider.html | 1 + .../_mdf.js_tasks.DefaultPollingGroups.html | 2 + docs/types/_mdf.js_tasks.DoneListener.html | 8 + .../_mdf.js_tasks.MetricsDefinitions.html | 2 + docs/types/_mdf.js_tasks.PollingGroup.html | 3 + docs/types/_mdf.js_tasks.RetryStrategy.html | 2 + .../_mdf.js_tasks.ScanMetricsDefinitions.html | 13 + docs/types/_mdf.js_tasks.Strategy-1.html | 2 + docs/types/_mdf.js_tasks.TaskBaseConfig.html | 2 + .../_mdf.js_tasks.TaskMetricsDefinitions.html | 6 + docs/types/_mdf.js_tasks.TaskState.html | 2 + docs/types/_mdf.js_utils.Coercible.html | 2 + docs/types/_mdf.js_utils.Format.html | 2 + docs/types/_mdf.js_utils.FormatFunction.html | 2 + docs/types/_mdf.js_utils.LoggerFunction.html | 3 + docs/types/_mdf.js_utils.LoggerInstance.html | 2 + docs/types/_mdf.js_utils.TaskArguments.html | 2 + docs/types/_mdf.js_utils.TaskAsPromise.html | 3 + ...df_js_amqp_provider.Receiver.Provider.html | 1 - ..._mdf_js_amqp_provider.Sender.Provider.html | 1 - .../types/_mdf_js_core.Health.CheckEntry.html | 2 - docs/types/_mdf_js_core.Health.Checks.html | 31 - .../_mdf_js_core.Health.ComponentName.html | 2 - .../_mdf_js_core.Health.MeasurementName.html | 2 - docs/types/_mdf_js_core.Health.Status-1.html | 2 - docs/types/_mdf_js_core.Jobs.AnyHeaders.html | 2 - docs/types/_mdf_js_core.Jobs.AnyOptions.html | 2 - .../_mdf_js_core.Jobs.DoneEventHandler.html | 7 - docs/types/_mdf_js_core.Jobs.Headers.html | 2 - docs/types/_mdf_js_core.Jobs.Options.html | 2 - docs/types/_mdf_js_core.Layer.Observable.html | 2 - ..._js_core.Layer.Provider.ProviderState.html | 2 - docs/types/_mdf_js_crash.Cause.html | 2 - docs/types/_mdf_js_crash.ContextLink.html | 2 - docs/types/_mdf_js_crash.Links.html | 2 - docs/types/_mdf_js_crash.SimpleLink.html | 2 - .../_mdf_js_doorkeeper.ResultCallback.html | 2 - .../_mdf_js_doorkeeper.SchemaSelector.html | 1 - .../_mdf_js_doorkeeper.ValidatedOutput.html | 1 - ..._js_elastic_provider.Elastic.Provider.html | 1 - docs/types/_mdf_js_faker.Builder.html | 2 - docs/types/_mdf_js_faker.DefaultValue.html | 2 - docs/types/_mdf_js_faker.Dependencies.html | 2 - .../_mdf_js_firehose.JobEventHandler.html | 2 - .../_mdf_js_firehose.Plugs.Sink.Any.html | 1 - .../_mdf_js_firehose.Plugs.Sink.Tap.html | 1 - .../_mdf_js_firehose.Plugs.Source.Any.html | 1 - ...js_http_client_provider.HTTP.Provider.html | 1 - ...js_http_server_provider.HTTP.Provider.html | 1 - ...s_jsonl_archiver.JSONLArchiver.Config.html | 1 - ...jsonl_archiver.JSONLArchiver.Provider.html | 1 - ...f_js_kafka_provider.Consumer.Provider.html | 1 - ...f_js_kafka_provider.Producer.Provider.html | 1 - ..._mdf_js_logger.FluentdTransportConfig.html | 4 - ...js_middlewares.AfterRoutesMiddlewares.html | 1 - .../_mdf_js_middlewares.AuditCategory.html | 4 - ...s_middlewares.BeforeRoutesMiddlewares.html | 1 - ...f_js_middlewares.EndpointsMiddlewares.html | 1 - .../_mdf_js_middlewares.Middlewares.html | 1 - ..._mdf_js_mongo_provider.Mongo.Provider.html | 1 - .../_mdf_js_mqtt_provider.MQTT.Provider.html | 1 - .../_mdf_js_openc2.Adapters.Dummy.Config.html | 1 - .../_mdf_js_openc2.Adapters.Redis.Config.html | 1 - ...df_js_openc2.Adapters.SocketIO.Config.html | 1 - .../_mdf_js_openc2_core.CommandJobDone.html | 1 - ..._mdf_js_openc2_core.CommandJobHandler.html | 1 - ...openc2_core.Control.ActionTargetPairs.html | 2 - ...mdf_js_openc2_core.Control.ActionType.html | 1 - .../_mdf_js_openc2_core.Control.Message.html | 1 - ..._mdf_js_openc2_core.Control.Namespace.html | 2 - .../_mdf_js_openc2_core.OnCommandHandler.html | 1 - docs/types/_mdf_js_openc2_core.Resolver.html | 1 - .../_mdf_js_openc2_core.ResolverEntry.html | 1 - .../_mdf_js_openc2_core.ResolverMap.html | 1 - .../_mdf_js_redis_provider.Redis.Config.html | 1 - ..._mdf_js_redis_provider.Redis.Provider.html | 1 - .../_mdf_js_s3_provider.S3.Provider.html | 1 - ...rvice_registry.ConsumerAdapterOptions.html | 8 - ...mdf_js_service_registry.CustomSetting.html | 2 - ...df_js_service_registry.CustomSettings.html | 2 - .../_mdf_js_service_registry.ErrorRecord.html | 1 - ...service_setup_provider.Setup.Provider.html | 1 - ...ient_provider.SocketIOClient.Provider.html | 1 - ...rver_provider.SocketIOServer.Provider.html | 1 - .../_mdf_js_tasks.DefaultPollingGroups.html | 2 - docs/types/_mdf_js_tasks.DoneListener.html | 8 - .../_mdf_js_tasks.MetricsDefinitions.html | 2 - docs/types/_mdf_js_tasks.PollingGroup.html | 3 - docs/types/_mdf_js_tasks.RetryStrategy.html | 2 - docs/types/_mdf_js_tasks.Strategy-1.html | 2 - docs/types/_mdf_js_tasks.TaskBaseConfig.html | 2 - docs/types/_mdf_js_tasks.TaskState.html | 2 - docs/types/_mdf_js_utils.LoggerFunction.html | 3 - docs/types/_mdf_js_utils.TaskArguments.html | 2 - docs/types/_mdf_js_utils.TaskAsPromise.html | 3 - ...mdf.js_amqp-provider.Receiver.Factory.html | 1 + .../_mdf.js_amqp-provider.Sender.Factory.html | 1 + .../_mdf.js_core.Health.STATUSES.html | 14 + ...s_core.Layer.Provider.PROVIDER_STATES.html | 2 + ...f.js_elastic-provider.Elastic.Factory.html | 1 + ....js_http-client-provider.HTTP.Factory.html | 1 + ....js_http-server-provider.HTTP.Factory.html | 1 + ..._jsonl-archiver.JSONLArchiver.Factory.html | 1 + ...df.js_kafka-provider.Consumer.Factory.html | 1 + ...df.js_kafka-provider.Producer.Factory.html | 1 + docs/variables/_mdf.js_logger.LOG_LEVELS.html | 4 + docs/variables/_mdf.js_logger.default.html | 1 + .../_mdf.js_middlewares.Middleware.html | 1 + .../_mdf.js_mongo-provider.Mongo.Factory.html | 1 + .../_mdf.js_mqtt-provider.MQTT.Factory.html | 1 + ...f.js_openc2-core.Control.ACTION_TYPES.html | 1 + .../_mdf.js_redis-provider.Redis.Factory.html | 1 + .../_mdf.js_s3-provider.S3.Factory.html | 1 + ..._service-setup-provider.Setup.Factory.html | 1 + ...lient-provider.SocketIOClient.Factory.html | 1 + ...erver-provider.SocketIOServer.Factory.html | 1 + docs/variables/_mdf.js_tasks.STRATEGIES.html | 2 + docs/variables/_mdf.js_tasks.TASK_STATES.html | 2 + .../_mdf.js_utils.MAX_WAIT_TIME.html | 2 + docs/variables/_mdf.js_utils.WAIT_TIME.html | 2 + ...mdf_js_amqp_provider.Receiver.Factory.html | 1 - .../_mdf_js_amqp_provider.Sender.Factory.html | 1 - .../_mdf_js_core.Health.STATUSES.html | 14 - ...s_core.Layer.Provider.PROVIDER_STATES.html | 2 - ...f_js_elastic_provider.Elastic.Factory.html | 1 - ..._js_http_client_provider.HTTP.Factory.html | 1 - ..._js_http_server_provider.HTTP.Factory.html | 1 - ..._jsonl_archiver.JSONLArchiver.Factory.html | 1 - ...df_js_kafka_provider.Consumer.Factory.html | 1 - ...df_js_kafka_provider.Producer.Factory.html | 1 - docs/variables/_mdf_js_logger.default.html | 1 - .../_mdf_js_middlewares.Middleware.html | 1 - .../_mdf_js_mongo_provider.Mongo.Factory.html | 1 - .../_mdf_js_mqtt_provider.MQTT.Factory.html | 1 - ...f_js_openc2_core.Control.ACTION_TYPES.html | 1 - .../_mdf_js_redis_provider.Redis.Factory.html | 1 - .../_mdf_js_s3_provider.S3.Factory.html | 1 - ..._service_setup_provider.Setup.Factory.html | 1 - ...lient_provider.SocketIOClient.Factory.html | 1 - ...erver_provider.SocketIOServer.Factory.html | 1 - docs/variables/_mdf_js_tasks.STRATEGIES.html | 2 - docs/variables/_mdf_js_tasks.TASK_STATES.html | 2 - .../_mdf_js_utils.MAX_WAIT_TIME.html | 2 - docs/variables/_mdf_js_utils.WAIT_TIME.html | 2 - package.json | 169 +- packages/api/core/README.md | 1 + packages/api/core/package.json | 9 +- .../api/core/src/Health/overallStatus.test.ts | 195 +- packages/api/core/src/Jobs/JobHandler.ts | 565 ++-- .../api/core/src/Jobs/types/Strategy.i.ts | 56 +- .../api/core/src/Layer/Provider/Factory.ts | 115 +- .../api/core/src/Layer/Provider/Manager.ts | 776 ++--- packages/api/core/src/Layer/Provider/Port.ts | 470 +-- packages/api/crash/README.md | 1 + packages/api/crash/package.json | 103 +- packages/api/crash/src/Boom/BoomError.ts | 453 ++- packages/api/crash/src/Boom/BoomHelpers.ts | 1201 ++++---- packages/api/crash/src/Crash/CrashError.ts | 348 +-- .../api/crash/src/Multi/MultiError.test.ts | 703 ++--- packages/api/crash/src/Multi/MultiError.ts | 431 +-- packages/api/doorkeeper/README.md | 3 +- packages/api/doorkeeper/package.json | 113 +- packages/api/doorkeeper/src/Doorkeeper.ts | 880 +++--- packages/api/faker/README.md | 1 + packages/api/faker/package.json | 2 +- packages/api/faker/src/Factory.ts | 1187 ++++---- packages/api/file-flinger/.eslintignore | 3 + packages/api/file-flinger/.eslintrc.js | 7 + packages/api/file-flinger/README.md | 456 +++ packages/api/file-flinger/jest.config.js | 7 + packages/api/file-flinger/package.json | 57 + .../api/file-flinger/src/FileFinger.test.ts | 362 +++ packages/api/file-flinger/src/FileFlinger.ts | 150 + .../file-flinger/src/engine/Engine.test.ts | 215 ++ .../api/file-flinger/src/engine/Engine.ts | 233 ++ .../file-flinger/src/engine/FileTasks.test.ts | 633 ++++ .../api/file-flinger/src/engine/FileTasks.ts | 423 +++ packages/api/file-flinger/src/engine/index.ts | 9 + .../src/engine/types/EngineOptions.i.ts | 18 + .../src/engine/types/ErrorStrategy.t..ts | 23 + .../src/engine/types/ErroredFile.i.ts | 18 + .../src/engine/types/FileTaskIdentifiers.t.ts | 15 + .../src/engine/types/FileTasksOptions.i.ts | 27 + .../engine/types/PostProcessingStrategy.t.ts | 23 + .../file-flinger/src/engine/types/const.ts | 44 + .../file-flinger/src/engine/types/index.ts | 14 + packages/api/file-flinger/src/index.ts | 15 + .../file-flinger/src/keygen/Keygen.test.ts | 95 + .../api/file-flinger/src/keygen/Keygen.ts | 147 + packages/api/file-flinger/src/keygen/index.ts | 9 + .../src/keygen/types/KeygenOptions.i.ts | 37 + .../file-flinger/src/keygen/types/const.ts | 18 + .../file-flinger/src/keygen/types/index.ts | 9 + .../src/metrics/MetricsHandler.ts | 82 + .../api/file-flinger/src/metrics/index.ts | 8 + .../src/pusher/PusherWrapper.test.ts | 141 + .../file-flinger/src/pusher/PusherWrapper.ts | 148 + packages/api/file-flinger/src/pusher/index.ts | 9 + .../file-flinger/src/pusher/types/Pusher.i.ts | 18 + .../file-flinger/src/pusher/types/index.ts | 8 + .../src/types/FileFlingerOptions.i.ts | 19 + packages/api/file-flinger/src/types/const.ts | 33 + packages/api/file-flinger/src/types/index.ts | 9 + .../file-flinger/src/watcher/Watcher.test.ts | 307 ++ .../api/file-flinger/src/watcher/Watcher.ts | 286 ++ .../api/file-flinger/src/watcher/index.ts | 9 + .../src/watcher/types/WatcherOptions.i.ts | 25 + .../file-flinger/src/watcher/types/const.ts | 42 + .../file-flinger/src/watcher/types/index.ts | 9 + packages/api/file-flinger/stryker.conf.js | 8 + packages/api/file-flinger/tsconfig.build.json | 9 + packages/api/file-flinger/tsconfig.json | 8 + packages/api/file-flinger/tsconfig.lint.json | 4 + packages/api/file-flinger/tsconfig.spec.json | 8 + packages/api/file-flinger/typedoc.json | 34 + packages/api/firehose/README.md | 583 +++- .../api/firehose/media/firehose-diagram.svg | 3 + packages/api/firehose/package.json | 105 +- packages/api/firehose/src/Engine/Engine.ts | 2 +- packages/api/firehose/src/Firehose.ts | 33 +- .../api/firehose/src/Sink/core/PlugWrapper.ts | 318 +- packages/api/firehose/src/index.ts | 24 +- .../firehose/src/metrics/MetricsHandler.ts | 243 +- .../firehose/src/types/JobEventHandler.t.ts | 16 + .../firehose/src/types/Plugs/Sink/Jet.i.ts | 48 +- .../firehose/src/types/Plugs/Sink/Tap.i.ts | 36 +- .../src/types/Plugs/Source/CreditsFlow.i.ts | 6 +- .../firehose/src/types/Plugs/Source/Flow.i.ts | 48 +- .../src/types/Plugs/Source/Sequence.i.ts | 58 +- packages/api/firehose/src/types/index.ts | 47 +- packages/api/logger/README.md | 257 +- packages/api/logger/media/logging-capture.png | Bin 0 -> 137218 bytes packages/api/logger/package.json | 107 +- packages/api/logger/src/formats/formats.ts | 96 +- packages/api/logger/src/index.ts | 32 +- .../api/logger/src/wrapper/Wrapper.test.ts | 143 +- packages/api/logger/src/wrapper/Wrapper.ts | 232 +- packages/api/middlewares/README.md | 1 + packages/api/middlewares/package.json | 14 +- packages/api/middlewares/src/authz/authz.ts | 315 +- packages/api/middlewares/src/authz/index.ts | 14 +- packages/api/middlewares/src/cache/Cache.ts | 458 +-- .../middlewares/src/cache/CacheRepository.ts | 177 +- packages/api/middlewares/src/cors/cors.ts | 204 +- packages/api/middlewares/src/index.ts | 124 +- packages/api/middlewares/src/logger/logger.ts | 143 +- .../api/middlewares/src/metrics/metrics.ts | 500 +-- packages/api/middlewares/src/multer/multer.ts | 541 ++-- .../src/rateLimiter/RateLimitConfig.i.ts | 41 +- .../src/rateLimiter/rateLimiter.ts | 143 +- packages/api/openc2-core/README.md | 1 + packages/api/openc2-core/package.json | 9 +- .../openc2-core/src/Router/oc2.controller.ts | 278 +- .../src/components/Consumer/Consumer.ts | 779 +++-- .../src/components/Gateway/Gateway.ts | 833 ++--- .../src/components/Producer/ConsumerMap.ts | 408 +-- .../src/components/Producer/Producer.ts | 1041 ++++--- .../src/components/Producer/index.ts | 17 +- .../api/openc2-core/src/helpers/Accessors.ts | 218 +- .../api/openc2-core/src/helpers/Checkers.ts | 446 ++- packages/api/openc2-core/src/index.ts | 47 +- packages/api/tasks/README.md | 1 + packages/api/tasks/package.json | 109 +- packages/api/tasks/src/Helpers/Validator.ts | 502 ++-- .../api/tasks/src/Limiter/Limiter.test.ts | 1868 ++++++------ .../tasks/src/Limiter/LimiterStateHandler.ts | 586 ++-- packages/api/tasks/src/Limiter/Queue.ts | 648 ++-- .../src/Limiter/types/LimiterOptions.i.ts | 85 +- .../tasks/src/Limiter/types/QueueOptions.i.ts | 103 +- .../api/tasks/src/Polling/PollingExecutor.ts | 16 +- .../api/tasks/src/Polling/PollingManager.ts | 8 +- .../tasks/src/Polling/PollingStatsManager.ts | 4 +- .../api/tasks/src/Polling/RetryManager.ts | 163 +- .../src/Polling/types/MetricsDefinitions.t.ts | 361 +-- .../Polling/types/PollingManagerOptions.i.ts | 4 +- .../api/tasks/src/Scheduler/Scheduler.test.ts | 2 +- packages/api/tasks/src/Scheduler/Scheduler.ts | 11 +- .../src/Scheduler/types/SchedulerOptions.i.ts | 4 +- packages/api/tasks/src/Tasks/Group.ts | 148 +- packages/api/tasks/src/Tasks/Sequence.ts | 103 +- packages/api/tasks/src/Tasks/TaskHandler.ts | 597 ++-- .../tasks/src/Tasks/types/TaskOptions.i.ts | 87 +- packages/api/tasks/src/index.ts | 103 +- packages/api/utils/README.md | 1 + packages/api/utils/package.json | 103 +- packages/api/utils/src/camelCase/Options.i.ts | 85 +- packages/api/utils/src/coerce/coerce.ts | 137 +- packages/api/utils/src/formatEnv/index.ts | 18 +- .../api/utils/src/formatEnv/types/Format.t.ts | 18 +- .../src/formatEnv/types/FormatFunction.t.ts | 18 +- .../src/formatEnv/types/ReadEnvOptions.i.ts | 35 +- packages/api/utils/src/loadFile/loadFile.ts | 83 +- packages/components/openc2/README.md | 1 + packages/components/openc2/package.json | 10 +- .../adapters/Dummy/DummyConsumerAdapter.ts | 74 +- .../adapters/Dummy/DummyProducerAdapter.ts | 62 +- .../Redis/RedisConsumerAdapter.test.ts | 665 ++-- .../Redis/RedisProducerAdapter.test.ts | 487 +-- packages/components/openc2/src/index.ts | 56 +- .../openc2/src/serviceBus/ServiceBus.ts | 513 ++-- .../src/serviceBus/middlewares/authz/authz.ts | 205 +- .../openc2/src/types/AdapterOptions.i.ts | 36 +- .../src/types/ProviderConfigOptions.t.ts | 32 +- .../components/service-registry/README.md | 16 +- .../components/service-registry/package.json | 9 +- .../src/ServiceRegistry.test.ts | 2566 ++++++++-------- .../service-registry/src/ServiceRegistry.ts | 868 +++--- .../src/control/ControlManager.ts | 527 ++-- .../components/service-registry/src/index.ts | 50 +- .../src/observability/Observability.ts | 359 +-- .../observability/ObservabilityAppManager.ts | 417 +-- .../src/observability/index.ts | 35 +- .../registries/errors/Ports/MasterPort.ts | 344 +-- .../registries/errors/RegisterFacade.ts | 363 +-- .../observability/registries/errors/index.ts | 31 +- .../registries/errors/types/ErrorRecord.t.ts | 43 +- .../registries/health/Ports/MasterPort.ts | 411 +-- .../registries/metrics/Router/metrics.test.ts | 435 +-- .../metrics/Router/metrics.validator.ts | 75 +- .../src/settings/Router/config.test.ts | 359 +-- .../src/settings/SettingsManager.test.ts | 356 +-- .../src/settings/types/const.ts | 30 +- .../src/types/BootstrapSettings.i.ts | 182 +- packages/providers/amqp/README.md | 1 + packages/providers/amqp/package.json | 108 +- .../amqp/src/Common/config/default.ts | 109 +- packages/providers/amqp/src/Receiver/Port.ts | 44 +- .../amqp/src/Receiver/config/default.ts | 3 +- packages/providers/amqp/src/Receiver/index.ts | 20 +- packages/providers/amqp/src/Sender/Port.ts | 44 +- .../amqp/src/Sender/config/default.ts | 3 +- packages/providers/amqp/src/Sender/index.ts | 19 +- .../amqp/src/base/protocol/Frame/index.ts | 17 +- .../protocol/Primitives/Serializer/Parser.ts | 633 ++-- .../amqp/src/base/protocol/Version/Handler.ts | 65 +- .../src/base/protocol/Version/Serializer.ts | 77 +- .../amqp/src/base/protocol/Version/index.ts | 21 +- packages/providers/elastic/README.md | 3 +- packages/providers/elastic/package.json | 4 +- .../providers/elastic/src/config/default.ts | 10 +- packages/providers/elastic/src/index.ts | 44 +- .../elastic/src/provider/Status.t.ts | 76 +- packages/providers/http-client/README.md | 78 + packages/providers/http-client/package.json | 104 +- .../http-client/src/config/schema.ts | 79 +- .../providers/http-client/src/config/utils.ts | 102 +- .../http-client/src/provider/index.ts | 20 +- packages/providers/http-server/README.md | 59 + packages/providers/http-server/package.json | 6 +- .../http-server/src/provider/index.ts | 20 +- .../src/provider/types/Config.t.ts | 30 +- packages/providers/jsonl-archiver/README.md | 1 + .../providers/jsonl-archiver/package.json | 10 +- .../src/Client/ArchiverManager.ts | 2 +- .../jsonl-archiver/src/Client/FileHandler.ts | 1 + .../src/Client/types/ArchiveOptions.i.ts | 30 +- .../providers/jsonl-archiver/src/index.ts | 2 +- .../jsonl-archiver/src/provider/index.ts | 20 +- packages/providers/kafka/README.md | 4 +- packages/providers/kafka/package.json | 107 +- packages/providers/kafka/src/Client/Client.ts | 460 +-- .../kafka/src/Common/config/default.ts | 10 +- .../providers/kafka/src/Common/config/env.ts | 5 +- .../kafka/src/Common/config/utils.ts | 3 +- .../providers/kafka/src/Consumer/index.ts | 20 +- .../providers/kafka/src/Producer/index.ts | 20 +- packages/providers/mongo/README.md | 2 + packages/providers/mongo/package.json | 4 +- packages/providers/mongo/src/provider/Port.ts | 487 +-- .../providers/mongo/src/provider/index.ts | 22 +- packages/providers/mqtt/README.md | 4 +- packages/providers/mqtt/package.json | 4 +- packages/providers/mqtt/src/config/default.ts | 10 +- packages/providers/mqtt/src/provider/index.ts | 20 +- packages/providers/redis/README.md | 4 +- packages/providers/redis/package.json | 2 +- packages/providers/redis/src/config/env.ts | 8 +- packages/providers/redis/src/provider/Port.ts | 717 ++--- .../redis/src/provider/Provider.test.ts | 996 +++--- .../providers/redis/src/provider/index.ts | 20 +- packages/providers/s3/README.md | 1 + packages/providers/s3/package.json | 102 +- packages/providers/s3/src/provider/Port.ts | 118 +- packages/providers/s3/src/provider/index.ts | 20 +- packages/providers/service-setup/README.md | 1 + packages/providers/service-setup/package.json | 118 +- .../service-setup/src/provider/index.ts | 20 +- packages/providers/socket-client/README.md | 2 + packages/providers/socket-client/package.json | 6 +- .../socket-client/src/provider/Port.ts | 266 +- .../socket-client/src/provider/index.ts | 20 +- packages/providers/socket-server/README.md | 2 + packages/providers/socket-server/package.json | 8 +- .../socket-server/src/provider/index.ts | 29 +- .../src/provider/types/InstrumentOptions.i.ts | 76 +- .../socket-server/src/provider/types/index.ts | 22 +- .../tools/repo-config/src/getJestConfig.js | 64 +- test.mjs | 50 - turbo.json | 65 +- typedoc.json | 43 +- 1088 files changed, 37738 insertions(+), 27586 deletions(-) create mode 100644 docs/assets/hierarchy.js create mode 100644 docs/assets/media/Provider-Class-Hierarchy.png create mode 100644 docs/assets/media/Provider-States-Events.png create mode 100644 docs/assets/media/Provider-States-Methods.png create mode 100644 docs/assets/media/firehose-diagram.svg create mode 100644 docs/assets/media/logging-capture.png create mode 100644 docs/classes/_mdf.js_amqp-provider.Receiver.Port.html create mode 100644 docs/classes/_mdf.js_amqp-provider.Sender.Port.html create mode 100644 docs/classes/_mdf.js_amqp-provider._internal_.BasePort.html create mode 100644 docs/classes/_mdf.js_amqp-provider._internal_.Container.html create mode 100644 docs/classes/_mdf.js_amqp-provider._internal_.Receiver.html create mode 100644 docs/classes/_mdf.js_amqp-provider._internal_.Sender.html create mode 100644 docs/classes/_mdf.js_core.Jobs.JobHandler.html create mode 100644 docs/classes/_mdf.js_core.Layer.Provider.Manager.html create mode 100644 docs/classes/_mdf.js_core.Layer.Provider.Port.html create mode 100644 docs/classes/_mdf.js_crash.Boom.html rename docs/classes/{_mdf_js_crash.BoomHelpers.html => _mdf.js_crash.BoomHelpers.html} (52%) create mode 100644 docs/classes/_mdf.js_crash.Crash.html create mode 100644 docs/classes/_mdf.js_crash.Multi.html create mode 100644 docs/classes/_mdf.js_crash._internal_.Base.html create mode 100644 docs/classes/_mdf.js_doorkeeper.DoorKeeper.html rename docs/classes/{_mdf_js_elastic_provider.Elastic.Port.html => _mdf.js_elastic-provider.Elastic.Port.html} (52%) create mode 100644 docs/classes/_mdf.js_faker.Factory.html create mode 100644 docs/classes/_mdf.js_file-flinger.FileFlinger.html create mode 100644 docs/classes/_mdf.js_file-flinger._internal_.Engine.html create mode 100644 docs/classes/_mdf.js_file-flinger._internal_.FileTasks.html create mode 100644 docs/classes/_mdf.js_file-flinger._internal_.Keygen.html create mode 100644 docs/classes/_mdf.js_file-flinger._internal_.MetricsHandler.html create mode 100644 docs/classes/_mdf.js_file-flinger._internal_.Watcher.html create mode 100644 docs/classes/_mdf.js_firehose.Firehose.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Base-1.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Base.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.CreditsFlow.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Engine.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Flow.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Jet.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.MetricsHandler.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.PlugWrapper-1.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.PlugWrapper.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Sequence.html create mode 100644 docs/classes/_mdf.js_firehose._internal_.Tap.html create mode 100644 docs/classes/_mdf.js_http-client-provider.HTTP.Port.html create mode 100644 docs/classes/_mdf.js_http-server-provider.HTTP.Port.html create mode 100644 docs/classes/_mdf.js_jsonl-archiver.ArchiverManager.html create mode 100644 docs/classes/_mdf.js_jsonl-archiver.JSONLArchiver.Port.html create mode 100644 docs/classes/_mdf.js_jsonl-archiver._internal_.FileHandler.html create mode 100644 docs/classes/_mdf.js_kafka-provider.Consumer.Port.html create mode 100644 docs/classes/_mdf.js_kafka-provider.Producer.Port.html create mode 100644 docs/classes/_mdf.js_kafka-provider._internal_.BasePort.html create mode 100644 docs/classes/_mdf.js_kafka-provider._internal_.Client.html create mode 100644 docs/classes/_mdf.js_kafka-provider._internal_.Consumer.html create mode 100644 docs/classes/_mdf.js_kafka-provider._internal_.Producer.html create mode 100644 docs/classes/_mdf.js_logger.DebugLogger.html rename docs/classes/{_mdf_js_logger.Logger.html => _mdf.js_logger.Logger.html} (51%) create mode 100644 docs/classes/_mdf.js_logger.WrapperLogger.html create mode 100644 docs/classes/_mdf.js_middlewares.Audit.html create mode 100644 docs/classes/_mdf.js_middlewares.AuthZ.html create mode 100644 docs/classes/_mdf.js_middlewares.BodyParser.html rename docs/classes/{_mdf_js_middlewares.Cache.html => _mdf.js_middlewares.Cache.html} (51%) rename docs/classes/{_mdf_js_middlewares.Cors.html => _mdf.js_middlewares.Cors.html} (52%) create mode 100644 docs/classes/_mdf.js_middlewares.Default.html create mode 100644 docs/classes/_mdf.js_middlewares.ErrorHandler.html create mode 100644 docs/classes/_mdf.js_middlewares.Logger.html rename docs/classes/{_mdf_js_middlewares.Metrics.html => _mdf.js_middlewares.Metrics.html} (50%) create mode 100644 docs/classes/_mdf.js_middlewares.Multer.html create mode 100644 docs/classes/_mdf.js_middlewares.NoCache.html rename docs/classes/{_mdf_js_middlewares.RateLimiter.html => _mdf.js_middlewares.RateLimiter.html} (54%) create mode 100644 docs/classes/_mdf.js_middlewares.RequestId.html create mode 100644 docs/classes/_mdf.js_middlewares.Security.html create mode 100644 docs/classes/_mdf.js_middlewares._internal_.CacheRepository.html create mode 100644 docs/classes/_mdf.js_mongo-provider.Mongo.Port.html create mode 100644 docs/classes/_mdf.js_mqtt-provider.MQTT.Port.html create mode 100644 docs/classes/_mdf.js_openc2-core.Accessors.html create mode 100644 docs/classes/_mdf.js_openc2-core.Consumer.html create mode 100644 docs/classes/_mdf.js_openc2-core.ConsumerMap.html create mode 100644 docs/classes/_mdf.js_openc2-core.Gateway.html create mode 100644 docs/classes/_mdf.js_openc2-core.Producer.html rename docs/classes/{_mdf_js_openc2_core.Registry.html => _mdf.js_openc2-core.Registry.html} (56%) create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper-1.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.Component.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.Controller.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.HealthWrapper.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.Model.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.Router.html create mode 100644 docs/classes/_mdf.js_openc2-core._internal_.Service.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyConsumerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyProducerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.Redis.RedisConsumerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.Redis.RedisProducerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html create mode 100644 docs/classes/_mdf.js_openc2.Factory.Consumer.html create mode 100644 docs/classes/_mdf.js_openc2.Factory.GatewayFactory.html create mode 100644 docs/classes/_mdf.js_openc2.Factory.Producer.html create mode 100644 docs/classes/_mdf.js_openc2.ServiceBus.html create mode 100644 docs/classes/_mdf.js_openc2._internal_.Adapter.html create mode 100644 docs/classes/_mdf.js_openc2._internal_.AddressMapper.html create mode 100644 docs/classes/_mdf.js_openc2._internal_.DummyAdapter.html create mode 100644 docs/classes/_mdf.js_openc2._internal_.RedisAdapter.html create mode 100644 docs/classes/_mdf.js_openc2._internal_.SocketIOAdapter.html create mode 100644 docs/classes/_mdf.js_redis-provider.Redis.Port.html create mode 100644 docs/classes/_mdf.js_s3-provider.S3.Port.html create mode 100644 docs/classes/_mdf.js_service-registry.ServiceRegistry.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Aggregator-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Aggregator-2.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Aggregator.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.ControlManager.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Controller-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Controller-2.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Controller-3.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Controller.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.HealthFacade.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.MetricsFacade.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Model-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Model-2.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Model-3.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Model.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Observability.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.ObservabilityAppManager.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Port-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Port.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.RegisterFacade.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Router-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Router-2.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Router-3.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Router.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Service-1.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Service-2.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Service-3.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Service.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.SettingsManagerAccessors.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.SettingsManagerBase.html create mode 100644 docs/classes/_mdf.js_service-registry._internal_.Validator.html create mode 100644 docs/classes/_mdf.js_service-setup-provider.ConfigManager.html create mode 100644 docs/classes/_mdf.js_service-setup-provider.Setup.Port.html create mode 100644 docs/classes/_mdf.js_socket-client-provider.SocketIOClient.Port.html create mode 100644 docs/classes/_mdf.js_socket-server-provider.SocketIOServer.Port.html create mode 100644 docs/classes/_mdf.js_tasks.Group.html create mode 100644 docs/classes/_mdf.js_tasks.Limiter.html create mode 100644 docs/classes/_mdf.js_tasks.PollingExecutor.html create mode 100644 docs/classes/_mdf.js_tasks.Scheduler.html create mode 100644 docs/classes/_mdf.js_tasks.Sequence.html create mode 100644 docs/classes/_mdf.js_tasks.Single.html create mode 100644 docs/classes/_mdf.js_tasks.TaskHandler.html create mode 100644 docs/classes/_mdf.js_tasks._internal_.LimiterStateHandler.html create mode 100644 docs/classes/_mdf.js_tasks._internal_.PollingManager.html create mode 100644 docs/classes/_mdf.js_tasks._internal_.PollingMetricsHandler.html create mode 100644 docs/classes/_mdf.js_tasks._internal_.Queue.html create mode 100644 docs/classes/_mdf.js_tasks._internal_.RetryManager.html delete mode 100644 docs/classes/_mdf_js_core.Jobs.JobHandler.html delete mode 100644 docs/classes/_mdf_js_core.Layer.Provider.Manager.html delete mode 100644 docs/classes/_mdf_js_core.Layer.Provider.Port.html delete mode 100644 docs/classes/_mdf_js_crash.Boom.html delete mode 100644 docs/classes/_mdf_js_crash.Crash.html delete mode 100644 docs/classes/_mdf_js_crash.Multi.html delete mode 100644 docs/classes/_mdf_js_doorkeeper.DoorKeeper.html delete mode 100644 docs/classes/_mdf_js_faker.Factory.html delete mode 100644 docs/classes/_mdf_js_firehose.Firehose.html delete mode 100644 docs/classes/_mdf_js_jsonl_archiver.ArchiverManager.html delete mode 100644 docs/classes/_mdf_js_logger.DebugLogger.html delete mode 100644 docs/classes/_mdf_js_middlewares.Audit.html delete mode 100644 docs/classes/_mdf_js_middlewares.AuthZ.html delete mode 100644 docs/classes/_mdf_js_middlewares.BodyParser.html delete mode 100644 docs/classes/_mdf_js_middlewares.Default.html delete mode 100644 docs/classes/_mdf_js_middlewares.ErrorHandler.html delete mode 100644 docs/classes/_mdf_js_middlewares.Logger.html delete mode 100644 docs/classes/_mdf_js_middlewares.Multer.html delete mode 100644 docs/classes/_mdf_js_middlewares.NoCache.html delete mode 100644 docs/classes/_mdf_js_middlewares.RequestId.html delete mode 100644 docs/classes/_mdf_js_middlewares.Security.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyConsumerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyProducerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.Redis.RedisConsumerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.Redis.RedisProducerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html delete mode 100644 docs/classes/_mdf_js_openc2.Factory.Consumer.html delete mode 100644 docs/classes/_mdf_js_openc2.Factory.GatewayFactory.html delete mode 100644 docs/classes/_mdf_js_openc2.Factory.Producer.html delete mode 100644 docs/classes/_mdf_js_openc2.ServiceBus.html delete mode 100644 docs/classes/_mdf_js_openc2_core.Accessors.html delete mode 100644 docs/classes/_mdf_js_openc2_core.Consumer.html delete mode 100644 docs/classes/_mdf_js_openc2_core.Gateway.html delete mode 100644 docs/classes/_mdf_js_openc2_core.Producer.html delete mode 100644 docs/classes/_mdf_js_service_registry.ServiceRegistry.html delete mode 100644 docs/classes/_mdf_js_service_setup_provider.ConfigManager.html delete mode 100644 docs/classes/_mdf_js_tasks.Group.html delete mode 100644 docs/classes/_mdf_js_tasks.Limiter.html delete mode 100644 docs/classes/_mdf_js_tasks.PollingExecutor.html delete mode 100644 docs/classes/_mdf_js_tasks.Scheduler.html delete mode 100644 docs/classes/_mdf_js_tasks.Sequence.html delete mode 100644 docs/classes/_mdf_js_tasks.Single.html delete mode 100644 docs/classes/_mdf_js_tasks.TaskHandler.html rename docs/enums/{_mdf_js_core.Health.STATUS.html => _mdf.js_core.Health.STATUS.html} (58%) rename docs/enums/{_mdf_js_core.Jobs.Status.html => _mdf.js_core.Jobs.Status.html} (63%) rename docs/enums/{_mdf_js_core.Layer.Provider.ProviderStatus.html => _mdf.js_core.Layer.Provider.ProviderStatus.html} (66%) create mode 100644 docs/enums/_mdf.js_file-flinger.ErrorStrategy.html create mode 100644 docs/enums/_mdf.js_file-flinger.PostProcessingStrategy.html rename docs/enums/{_mdf_js_openc2_core.Control.Action.html => _mdf.js_openc2-core.Control.Action.html} (69%) rename docs/enums/{_mdf_js_openc2_core.Control.Features.html => _mdf.js_openc2-core.Control.Features.html} (60%) rename docs/enums/{_mdf_js_openc2_core.Control.L4Protocol.html => _mdf.js_openc2-core.Control.L4Protocol.html} (61%) rename docs/enums/{_mdf_js_openc2_core.Control.MessageType.html => _mdf.js_openc2-core.Control.MessageType.html} (52%) rename docs/enums/{_mdf_js_openc2_core.Control.ResponseType.html => _mdf.js_openc2-core.Control.ResponseType.html} (61%) rename docs/enums/{_mdf_js_openc2_core.Control.StatusCode.html => _mdf.js_openc2-core.Control.StatusCode.html} (59%) rename docs/enums/{_mdf_js_tasks.LimiterState.html => _mdf.js_tasks.LimiterState.html} (63%) rename docs/enums/{_mdf_js_tasks.RETRY_STRATEGY.html => _mdf.js_tasks.RETRY_STRATEGY.html} (53%) rename docs/enums/{_mdf_js_tasks.STRATEGY.html => _mdf.js_tasks.STRATEGY.html} (64%) rename docs/enums/{_mdf_js_tasks.TASK_STATE.html => _mdf.js_tasks.TASK_STATE.html} (63%) create mode 100644 docs/functions/_mdf.js_core.Health.overallStatus.html create mode 100644 docs/functions/_mdf.js_core.Layer.Provider.ProviderFactoryCreator.html create mode 100644 docs/functions/_mdf.js_logger.SetContext.html create mode 100644 docs/functions/_mdf.js_utils.coerce.html create mode 100644 docs/functions/_mdf.js_utils.deCycle.html create mode 100644 docs/functions/_mdf.js_utils.escapeRegExp.html create mode 100644 docs/functions/_mdf.js_utils.findNodeModule.html create mode 100644 docs/functions/_mdf.js_utils.formatEnv.html create mode 100644 docs/functions/_mdf.js_utils.loadFile.html create mode 100644 docs/functions/_mdf.js_utils.prettyMS.html create mode 100644 docs/functions/_mdf.js_utils.retroCycle.html create mode 100644 docs/functions/_mdf.js_utils.retry.html create mode 100644 docs/functions/_mdf.js_utils.retryBind.html create mode 100644 docs/functions/_mdf.js_utils.wrapOnRetry.html delete mode 100644 docs/functions/_mdf_js_core.Health.overallStatus.html delete mode 100644 docs/functions/_mdf_js_core.Layer.Provider.ProviderFactoryCreator.html delete mode 100644 docs/functions/_mdf_js_logger.SetContext.html delete mode 100644 docs/functions/_mdf_js_utils.coerce.html delete mode 100644 docs/functions/_mdf_js_utils.deCycle.html delete mode 100644 docs/functions/_mdf_js_utils.escapeRegExp.html delete mode 100644 docs/functions/_mdf_js_utils.findNodeModule.html delete mode 100644 docs/functions/_mdf_js_utils.formatEnv.html delete mode 100644 docs/functions/_mdf_js_utils.loadFile.html delete mode 100644 docs/functions/_mdf_js_utils.prettyMS.html delete mode 100644 docs/functions/_mdf_js_utils.retroCycle.html delete mode 100644 docs/functions/_mdf_js_utils.retry.html delete mode 100644 docs/functions/_mdf_js_utils.retryBind.html delete mode 100644 docs/functions/_mdf_js_utils.wrapOnRetry.html rename docs/interfaces/{_mdf_js_core.Health.Check.html => _mdf.js_core.Health.Check.html} (52%) create mode 100644 docs/interfaces/_mdf.js_core.Jobs.DefaultOptions.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.JobObject.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.JobRequest.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.NoMoreHeaders.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.NoMoreOptions.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.Result.html create mode 100644 docs/interfaces/_mdf.js_core.Jobs.Strategy.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.App.Component.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.App.Health.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.App.Metadata.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.App.Resource.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.App.Service.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.Provider.Factory.html rename docs/interfaces/{_mdf_js_core.Layer.Provider.FactoryOptions.html => _mdf.js_core.Layer.Provider.FactoryOptions.html} (51%) create mode 100644 docs/interfaces/_mdf.js_core.Layer.Provider.PortConfigValidationStruct.html create mode 100644 docs/interfaces/_mdf.js_core.Layer.Provider.ProviderOptions.html create mode 100644 docs/interfaces/_mdf.js_core._internal_.State.html rename docs/interfaces/{_mdf_js_crash.APIError.html => _mdf.js_crash.APIError.html} (60%) create mode 100644 docs/interfaces/_mdf.js_crash.APISource.html create mode 100644 docs/interfaces/_mdf.js_crash.BoomOptions.html rename docs/interfaces/{_mdf_js_crash.Context.html => _mdf.js_crash.Context.html} (50%) create mode 100644 docs/interfaces/_mdf.js_crash.CrashObject.html create mode 100644 docs/interfaces/_mdf.js_crash.CrashOptions.html create mode 100644 docs/interfaces/_mdf.js_crash.MultiObject.html create mode 100644 docs/interfaces/_mdf.js_crash.MultiOptions.html create mode 100644 docs/interfaces/_mdf.js_crash.ValidationError.html create mode 100644 docs/interfaces/_mdf.js_crash.ValidationErrorItem.html create mode 100644 docs/interfaces/_mdf.js_crash._internal_.BaseObject.html create mode 100644 docs/interfaces/_mdf.js_crash._internal_.BaseOptions.html create mode 100644 docs/interfaces/_mdf.js_doorkeeper.DoorkeeperOptions.html rename docs/interfaces/{_mdf_js_faker.DefaultObject.html => _mdf.js_faker.DefaultObject.html} (50%) create mode 100644 docs/interfaces/_mdf.js_faker.DefaultOptions.html create mode 100644 docs/interfaces/_mdf.js_faker._internal_.Entry.html create mode 100644 docs/interfaces/_mdf.js_file-flinger.FileFlingerOptions.html create mode 100644 docs/interfaces/_mdf.js_file-flinger.Pusher.html create mode 100644 docs/interfaces/_mdf.js_file-flinger._internal_.EngineOptions.html create mode 100644 docs/interfaces/_mdf.js_file-flinger._internal_.ErroredFile.html create mode 100644 docs/interfaces/_mdf.js_file-flinger._internal_.FileTasksOptions.html create mode 100644 docs/interfaces/_mdf.js_file-flinger._internal_.KeygenOptions.html create mode 100644 docs/interfaces/_mdf.js_file-flinger._internal_.WatcherOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose.FirehoseOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose.Plugs.Sink.Jet.html create mode 100644 docs/interfaces/_mdf.js_firehose.Plugs.Sink.Tap.html create mode 100644 docs/interfaces/_mdf.js_firehose.Plugs.Source.CreditsFlow.html create mode 100644 docs/interfaces/_mdf.js_firehose.Plugs.Source.Flow.html create mode 100644 docs/interfaces/_mdf.js_firehose.Plugs.Source.Sequence.html create mode 100644 docs/interfaces/_mdf.js_firehose.PostConsumeOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.Base-2.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.Base-3.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.EngineOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.SinkOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.SourceOptions.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.WrappableSinkPlug.html create mode 100644 docs/interfaces/_mdf.js_firehose._internal_.WrappableSourcePlug.html create mode 100644 docs/interfaces/_mdf.js_http-client-provider.HTTP.Config.html create mode 100644 docs/interfaces/_mdf.js_http-server-provider.HTTP.Config.html create mode 100644 docs/interfaces/_mdf.js_jsonl-archiver.AppendResult.html create mode 100644 docs/interfaces/_mdf.js_jsonl-archiver.ArchiveOptions.html create mode 100644 docs/interfaces/_mdf.js_jsonl-archiver.FileStats.html create mode 100644 docs/interfaces/_mdf.js_jsonl-archiver._internal_.FileHandlerOptions.html create mode 100644 docs/interfaces/_mdf.js_kafka-provider.Consumer.Config.html rename docs/interfaces/{_mdf_js_kafka_provider.Producer.Config.html => _mdf.js_kafka-provider.Producer.Config.html} (54%) create mode 100644 docs/interfaces/_mdf.js_kafka-provider._internal_.BaseConfig.html rename docs/interfaces/{_mdf_js_logger.ConsoleTransportConfig.html => _mdf.js_logger.ConsoleTransportConfig.html} (53%) rename docs/interfaces/{_mdf_js_logger.FileTransportConfig.html => _mdf.js_logger.FileTransportConfig.html} (57%) rename docs/interfaces/{_mdf_js_logger.LoggerConfig.html => _mdf.js_logger.LoggerConfig.html} (51%) create mode 100644 docs/interfaces/_mdf.js_logger.LoggerInstance.html rename docs/interfaces/{_mdf_js_middlewares.AuditConfig.html => _mdf.js_middlewares.AuditConfig.html} (51%) create mode 100644 docs/interfaces/_mdf.js_middlewares.CacheConfig.html create mode 100644 docs/interfaces/_mdf.js_middlewares.CorsConfig.html create mode 100644 docs/interfaces/_mdf.js_middlewares.RateLimitConfig.html create mode 100644 docs/interfaces/_mdf.js_middlewares.RateLimitEntry.html create mode 100644 docs/interfaces/_mdf.js_mongo-provider.Mongo.Collections.html rename docs/interfaces/{_mdf_js_mongo_provider.Mongo.Config.html => _mdf.js_mongo-provider.Mongo.Config.html} (50%) create mode 100644 docs/interfaces/_mdf.js_mqtt-provider.MQTT.Config.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.CommandJobHeader.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.ConsumerAdapter.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.ConsumerOptions.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.Control.Actuator.html rename docs/interfaces/{_mdf_js_openc2_core.Control.Arguments.html => _mdf.js_openc2-core.Control.Arguments.html} (55%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Artifact.html => _mdf.js_openc2-core.Control.Artifact.html} (51%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Command.html => _mdf.js_openc2-core.Control.Command.html} (55%) create mode 100644 docs/interfaces/_mdf.js_openc2-core.Control.CommandMessage.html rename docs/interfaces/{_mdf_js_openc2_core.Control.Device.html => _mdf.js_openc2-core.Control.Device.html} (50%) rename docs/interfaces/{_mdf_js_openc2_core.Control.File.html => _mdf.js_openc2-core.Control.File.html} (55%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Hashes.html => _mdf.js_openc2-core.Control.Hashes.html} (56%) rename docs/interfaces/{_mdf_js_openc2_core.Control.IPv6Connection.html => _mdf.js_openc2-core.Control.IPv4Connection.html} (52%) rename docs/interfaces/{_mdf_js_openc2_core.Control.IPv4Connection.html => _mdf.js_openc2-core.Control.IPv6Connection.html} (52%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Payload.html => _mdf.js_openc2-core.Control.Payload.html} (52%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Process.html => _mdf.js_openc2-core.Control.Process.html} (57%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Response.html => _mdf.js_openc2-core.Control.Response.html} (52%) create mode 100644 docs/interfaces/_mdf.js_openc2-core.Control.ResponseMessage.html rename docs/interfaces/{_mdf_js_openc2_core.Control.Results.html => _mdf.js_openc2-core.Control.Results.html} (55%) rename docs/interfaces/{_mdf_js_openc2_core.Control.Target.html => _mdf.js_openc2-core.Control.Target.html} (60%) create mode 100644 docs/interfaces/_mdf.js_openc2-core.GatewayOptions.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.ProducerAdapter.html create mode 100644 docs/interfaces/_mdf.js_openc2-core.ProducerOptions.html create mode 100644 docs/interfaces/_mdf.js_openc2-core._internal_.BaseMessage.html create mode 100644 docs/interfaces/_mdf.js_openc2-core._internal_.ComponentAdapter.html create mode 100644 docs/interfaces/_mdf.js_openc2-core._internal_.ComponentOptions.html create mode 100644 docs/interfaces/_mdf.js_openc2-core._internal_.GatewayTimers.html create mode 100644 docs/interfaces/_mdf.js_openc2.AdapterOptions.html create mode 100644 docs/interfaces/_mdf.js_openc2.ServiceBusOptions.html create mode 100644 docs/interfaces/_mdf.js_redis-provider._internal_.MemoryStats.html create mode 100644 docs/interfaces/_mdf.js_redis-provider._internal_.ServerStats.html create mode 100644 docs/interfaces/_mdf.js_redis-provider._internal_.Status.html create mode 100644 docs/interfaces/_mdf.js_service-registry.BootstrapOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ExtendedCrashObject.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ExtendedMultiObject.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ObservabilityServiceOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ServiceRegistryOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ServiceRegistrySettings.html create mode 100644 docs/interfaces/_mdf.js_service-registry.ServiceSetting.html create mode 100644 docs/interfaces/_mdf.js_service-registry._internal_.HealthRegistryOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry._internal_.MetricsRegistryOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry._internal_.ObservabilityOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry._internal_.RegistryOptions.html create mode 100644 docs/interfaces/_mdf.js_service-registry._internal_.Response.html create mode 100644 docs/interfaces/_mdf.js_service-setup-provider.Setup.Config.html create mode 100644 docs/interfaces/_mdf.js_socket-client-provider.SocketIOClient.Config.html create mode 100644 docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.BasicAuthentication.html rename docs/interfaces/{_mdf_js_socket_server_provider.SocketIOServer.Config.html => _mdf.js_socket-server-provider.SocketIOServer.Config.html} (51%) create mode 100644 docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.ConnectionError.html create mode 100644 docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.InstrumentOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.ConsolidatedLimiterOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.GroupTaskBaseConfig.html create mode 100644 docs/interfaces/_mdf.js_tasks.LimiterOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.MetaData.html create mode 100644 docs/interfaces/_mdf.js_tasks.PollingManagerOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.PollingStats.html create mode 100644 docs/interfaces/_mdf.js_tasks.QueueOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.ResourceConfigEntry.html create mode 100644 docs/interfaces/_mdf.js_tasks.ResourcesConfigObject.html create mode 100644 docs/interfaces/_mdf.js_tasks.SchedulerOptions.html rename docs/interfaces/{_mdf_js_tasks.SequencePattern.html => _mdf.js_tasks.SequencePattern.html} (52%) create mode 100644 docs/interfaces/_mdf.js_tasks.SequenceTaskBaseConfig.html create mode 100644 docs/interfaces/_mdf.js_tasks.SingleTaskBaseConfig.html create mode 100644 docs/interfaces/_mdf.js_tasks.TaskOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks.WellIdentifiedTaskOptions.html create mode 100644 docs/interfaces/_mdf.js_tasks._internal_.ConsolidatedQueueOptions.html create mode 100644 docs/interfaces/_mdf.js_utils.ReadEnvOptions.html create mode 100644 docs/interfaces/_mdf.js_utils.RetryOptions.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.DefaultOptions.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.JobObject.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.JobRequest.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.NoMoreHeaders.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.NoMoreOptions.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.Result.html delete mode 100644 docs/interfaces/_mdf_js_core.Jobs.Strategy.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.App.Component.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.App.Health.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.App.Metadata.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.App.Resource.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.App.Service.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.Provider.Factory.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.Provider.PortConfigValidationStruct.html delete mode 100644 docs/interfaces/_mdf_js_core.Layer.Provider.ProviderOptions.html delete mode 100644 docs/interfaces/_mdf_js_crash.APISource.html delete mode 100644 docs/interfaces/_mdf_js_crash.BoomOptions.html delete mode 100644 docs/interfaces/_mdf_js_crash.CrashObject.html delete mode 100644 docs/interfaces/_mdf_js_crash.CrashOptions.html delete mode 100644 docs/interfaces/_mdf_js_crash.MultiObject.html delete mode 100644 docs/interfaces/_mdf_js_crash.MultiOptions.html delete mode 100644 docs/interfaces/_mdf_js_crash.ValidationError.html delete mode 100644 docs/interfaces/_mdf_js_crash.ValidationErrorItem.html delete mode 100644 docs/interfaces/_mdf_js_doorkeeper.DoorkeeperOptions.html delete mode 100644 docs/interfaces/_mdf_js_faker.DefaultOptions.html delete mode 100644 docs/interfaces/_mdf_js_firehose.FirehoseOptions.html delete mode 100644 docs/interfaces/_mdf_js_firehose.Plugs.Sink.Jet.html delete mode 100644 docs/interfaces/_mdf_js_firehose.Plugs.Source.CreditsFlow.html delete mode 100644 docs/interfaces/_mdf_js_firehose.Plugs.Source.Flow.html delete mode 100644 docs/interfaces/_mdf_js_firehose.Plugs.Source.Sequence.html delete mode 100644 docs/interfaces/_mdf_js_firehose.PostConsumeOptions.html delete mode 100644 docs/interfaces/_mdf_js_http_client_provider.HTTP.Config.html delete mode 100644 docs/interfaces/_mdf_js_http_server_provider.HTTP.Config.html delete mode 100644 docs/interfaces/_mdf_js_jsonl_archiver.ArchiveOptions.html delete mode 100644 docs/interfaces/_mdf_js_jsonl_archiver.FileStats.html delete mode 100644 docs/interfaces/_mdf_js_kafka_provider.Consumer.Config.html delete mode 100644 docs/interfaces/_mdf_js_logger.LoggerInstance.html delete mode 100644 docs/interfaces/_mdf_js_middlewares.CacheConfig.html delete mode 100644 docs/interfaces/_mdf_js_middlewares.CorsConfig.html delete mode 100644 docs/interfaces/_mdf_js_middlewares.RateLimitConfig.html delete mode 100644 docs/interfaces/_mdf_js_mongo_provider.Mongo.Collections.html delete mode 100644 docs/interfaces/_mdf_js_mqtt_provider.MQTT.Config.html delete mode 100644 docs/interfaces/_mdf_js_openc2.ServiceBusOptions.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.ConsumerAdapter.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.ConsumerOptions.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.Control.Actuator.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.Control.CommandMessage.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.Control.ResponseMessage.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.GatewayOptions.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.ProducerAdapter.html delete mode 100644 docs/interfaces/_mdf_js_openc2_core.ProducerOptions.html delete mode 100644 docs/interfaces/_mdf_js_service_registry.ObservabilityServiceOptions.html delete mode 100644 docs/interfaces/_mdf_js_service_registry.ServiceRegistryOptions.html delete mode 100644 docs/interfaces/_mdf_js_service_registry.ServiceRegistrySettings.html delete mode 100644 docs/interfaces/_mdf_js_service_registry.ServiceSetting.html delete mode 100644 docs/interfaces/_mdf_js_service_setup_provider.Setup.Config.html delete mode 100644 docs/interfaces/_mdf_js_socket_client_provider.SocketIOClient.Config.html delete mode 100644 docs/interfaces/_mdf_js_tasks.GroupTaskBaseConfig.html delete mode 100644 docs/interfaces/_mdf_js_tasks.LimiterOptions.html delete mode 100644 docs/interfaces/_mdf_js_tasks.MetaData.html delete mode 100644 docs/interfaces/_mdf_js_tasks.PollingManagerOptions.html delete mode 100644 docs/interfaces/_mdf_js_tasks.PollingStats.html delete mode 100644 docs/interfaces/_mdf_js_tasks.QueueOptions.html delete mode 100644 docs/interfaces/_mdf_js_tasks.ResourceConfigEntry.html delete mode 100644 docs/interfaces/_mdf_js_tasks.ResourcesConfigObject.html delete mode 100644 docs/interfaces/_mdf_js_tasks.SchedulerOptions.html delete mode 100644 docs/interfaces/_mdf_js_tasks.SequenceTaskBaseConfig.html delete mode 100644 docs/interfaces/_mdf_js_tasks.SingleTaskBaseConfig.html delete mode 100644 docs/interfaces/_mdf_js_tasks.TaskOptions.html delete mode 100644 docs/interfaces/_mdf_js_tasks.WellIdentifiedTaskOptions.html delete mode 100644 docs/interfaces/_mdf_js_utils.RetryOptions.html create mode 100644 docs/media/firehose-diagram.svg create mode 100644 docs/media/logging-capture-1.png create mode 100644 docs/modules.html create mode 100644 docs/modules/_mdf.js_amqp-provider.Receiver.html create mode 100644 docs/modules/_mdf.js_amqp-provider.Sender.html create mode 100644 docs/modules/_mdf.js_amqp-provider._internal_.html create mode 100644 docs/modules/_mdf.js_amqp-provider.html create mode 100644 docs/modules/_mdf.js_core.Health.html create mode 100644 docs/modules/_mdf.js_core.Jobs.html create mode 100644 docs/modules/_mdf.js_core.Layer.App.html create mode 100644 docs/modules/_mdf.js_core.Layer.Provider.html create mode 100644 docs/modules/_mdf.js_core.Layer.html create mode 100644 docs/modules/_mdf.js_core._internal_.html create mode 100644 docs/modules/_mdf.js_core.html create mode 100644 docs/modules/_mdf.js_crash._internal_.html create mode 100644 docs/modules/_mdf.js_crash.html create mode 100644 docs/modules/_mdf.js_doorkeeper.html create mode 100644 docs/modules/_mdf.js_elastic-provider.Elastic.html create mode 100644 docs/modules/_mdf.js_elastic-provider._internal_.html create mode 100644 docs/modules/_mdf.js_elastic-provider.html create mode 100644 docs/modules/_mdf.js_faker._internal_.html create mode 100644 docs/modules/_mdf.js_faker.html create mode 100644 docs/modules/_mdf.js_file-flinger._internal_.html create mode 100644 docs/modules/_mdf.js_file-flinger.html create mode 100644 docs/modules/_mdf.js_firehose.Plugs.Sink.html create mode 100644 docs/modules/_mdf.js_firehose.Plugs.Source.html create mode 100644 docs/modules/_mdf.js_firehose.Plugs.html create mode 100644 docs/modules/_mdf.js_firehose._internal_.html create mode 100644 docs/modules/_mdf.js_firehose.html create mode 100644 docs/modules/_mdf.js_http-client-provider.HTTP.html create mode 100644 docs/modules/_mdf.js_http-client-provider.html create mode 100644 docs/modules/_mdf.js_http-server-provider.HTTP.html create mode 100644 docs/modules/_mdf.js_http-server-provider.html create mode 100644 docs/modules/_mdf.js_jsonl-archiver.JSONLArchiver.html create mode 100644 docs/modules/_mdf.js_jsonl-archiver._internal_.html create mode 100644 docs/modules/_mdf.js_jsonl-archiver.html create mode 100644 docs/modules/_mdf.js_kafka-provider.Consumer.html create mode 100644 docs/modules/_mdf.js_kafka-provider.Producer.html create mode 100644 docs/modules/_mdf.js_kafka-provider._internal_.html create mode 100644 docs/modules/_mdf.js_kafka-provider.html create mode 100644 docs/modules/_mdf.js_logger.html create mode 100644 docs/modules/_mdf.js_middlewares._internal_.html create mode 100644 docs/modules/_mdf.js_middlewares.html create mode 100644 docs/modules/_mdf.js_mongo-provider.Mongo.html create mode 100644 docs/modules/_mdf.js_mongo-provider.html create mode 100644 docs/modules/_mdf.js_mqtt-provider.MQTT.html create mode 100644 docs/modules/_mdf.js_mqtt-provider.html create mode 100644 docs/modules/_mdf.js_openc2-core.Control.html create mode 100644 docs/modules/_mdf.js_openc2-core._internal_.html create mode 100644 docs/modules/_mdf.js_openc2-core.html create mode 100644 docs/modules/_mdf.js_openc2.Adapters.Dummy.html create mode 100644 docs/modules/_mdf.js_openc2.Adapters.Redis.html create mode 100644 docs/modules/_mdf.js_openc2.Adapters.SocketIO.html create mode 100644 docs/modules/_mdf.js_openc2.Adapters.html create mode 100644 docs/modules/_mdf.js_openc2.Factory.html create mode 100644 docs/modules/_mdf.js_openc2._internal_.html create mode 100644 docs/modules/_mdf.js_openc2.html create mode 100644 docs/modules/_mdf.js_redis-provider.Redis.html create mode 100644 docs/modules/_mdf.js_redis-provider._internal_.html create mode 100644 docs/modules/_mdf.js_redis-provider.html create mode 100644 docs/modules/_mdf.js_s3-provider.S3.html create mode 100644 docs/modules/_mdf.js_s3-provider.html create mode 100644 docs/modules/_mdf.js_service-registry._internal_.html create mode 100644 docs/modules/_mdf.js_service-registry.html create mode 100644 docs/modules/_mdf.js_service-setup-provider.Setup.html create mode 100644 docs/modules/_mdf.js_service-setup-provider._internal_.html create mode 100644 docs/modules/_mdf.js_service-setup-provider.html create mode 100644 docs/modules/_mdf.js_socket-client-provider.SocketIOClient.html create mode 100644 docs/modules/_mdf.js_socket-client-provider.html create mode 100644 docs/modules/_mdf.js_socket-server-provider.SocketIOServer.html create mode 100644 docs/modules/_mdf.js_socket-server-provider.html create mode 100644 docs/modules/_mdf.js_tasks._internal_.html create mode 100644 docs/modules/_mdf.js_tasks.html create mode 100644 docs/modules/_mdf.js_utils.html delete mode 100644 docs/modules/_mdf_js_amqp_provider.Receiver.html delete mode 100644 docs/modules/_mdf_js_amqp_provider.Sender.html delete mode 100644 docs/modules/_mdf_js_amqp_provider.html delete mode 100644 docs/modules/_mdf_js_core.Health.html delete mode 100644 docs/modules/_mdf_js_core.Jobs.html delete mode 100644 docs/modules/_mdf_js_core.Layer.App.html delete mode 100644 docs/modules/_mdf_js_core.Layer.Provider.html delete mode 100644 docs/modules/_mdf_js_core.Layer.html delete mode 100644 docs/modules/_mdf_js_core.html delete mode 100644 docs/modules/_mdf_js_crash.html delete mode 100644 docs/modules/_mdf_js_doorkeeper.html delete mode 100644 docs/modules/_mdf_js_elastic_provider.Elastic.html delete mode 100644 docs/modules/_mdf_js_elastic_provider.html delete mode 100644 docs/modules/_mdf_js_faker.html delete mode 100644 docs/modules/_mdf_js_firehose.Plugs.Sink.html delete mode 100644 docs/modules/_mdf_js_firehose.Plugs.Source.html delete mode 100644 docs/modules/_mdf_js_firehose.Plugs.html delete mode 100644 docs/modules/_mdf_js_firehose.html delete mode 100644 docs/modules/_mdf_js_http_client_provider.HTTP.html delete mode 100644 docs/modules/_mdf_js_http_client_provider.html delete mode 100644 docs/modules/_mdf_js_http_server_provider.HTTP.html delete mode 100644 docs/modules/_mdf_js_http_server_provider.html delete mode 100644 docs/modules/_mdf_js_jsonl_archiver.JSONLArchiver.html delete mode 100644 docs/modules/_mdf_js_jsonl_archiver.html delete mode 100644 docs/modules/_mdf_js_kafka_provider.Consumer.html delete mode 100644 docs/modules/_mdf_js_kafka_provider.Producer.html delete mode 100644 docs/modules/_mdf_js_kafka_provider.html delete mode 100644 docs/modules/_mdf_js_logger.html delete mode 100644 docs/modules/_mdf_js_middlewares.html delete mode 100644 docs/modules/_mdf_js_mongo_provider.Mongo.html delete mode 100644 docs/modules/_mdf_js_mongo_provider.html delete mode 100644 docs/modules/_mdf_js_mqtt_provider.MQTT.html delete mode 100644 docs/modules/_mdf_js_mqtt_provider.html delete mode 100644 docs/modules/_mdf_js_openc2.Adapters.Dummy.html delete mode 100644 docs/modules/_mdf_js_openc2.Adapters.Redis.html delete mode 100644 docs/modules/_mdf_js_openc2.Adapters.SocketIO.html delete mode 100644 docs/modules/_mdf_js_openc2.Adapters.html delete mode 100644 docs/modules/_mdf_js_openc2.Factory.html delete mode 100644 docs/modules/_mdf_js_openc2.html delete mode 100644 docs/modules/_mdf_js_openc2_core.Control.html delete mode 100644 docs/modules/_mdf_js_openc2_core.html delete mode 100644 docs/modules/_mdf_js_redis_provider.Redis.html delete mode 100644 docs/modules/_mdf_js_redis_provider.html delete mode 100644 docs/modules/_mdf_js_s3_provider.S3.html delete mode 100644 docs/modules/_mdf_js_s3_provider.html delete mode 100644 docs/modules/_mdf_js_service_registry.html delete mode 100644 docs/modules/_mdf_js_service_setup_provider.Setup.html delete mode 100644 docs/modules/_mdf_js_service_setup_provider.html delete mode 100644 docs/modules/_mdf_js_socket_client_provider.SocketIOClient.html delete mode 100644 docs/modules/_mdf_js_socket_client_provider.html delete mode 100644 docs/modules/_mdf_js_socket_server_provider.SocketIOServer.html delete mode 100644 docs/modules/_mdf_js_socket_server_provider.html delete mode 100644 docs/modules/_mdf_js_tasks.html delete mode 100644 docs/modules/_mdf_js_utils.html create mode 100644 docs/types/_mdf.js_amqp-provider.Receiver.Provider.html create mode 100644 docs/types/_mdf.js_amqp-provider.Sender.Provider.html create mode 100644 docs/types/_mdf.js_core.Health.CheckEntry.html create mode 100644 docs/types/_mdf.js_core.Health.Checks.html create mode 100644 docs/types/_mdf.js_core.Health.ComponentName.html create mode 100644 docs/types/_mdf.js_core.Health.MeasurementName.html create mode 100644 docs/types/_mdf.js_core.Health.Status-1.html create mode 100644 docs/types/_mdf.js_core.Jobs.AnyHeaders.html create mode 100644 docs/types/_mdf.js_core.Jobs.AnyOptions.html create mode 100644 docs/types/_mdf.js_core.Jobs.DoneEventHandler.html create mode 100644 docs/types/_mdf.js_core.Jobs.Headers.html create mode 100644 docs/types/_mdf.js_core.Jobs.Options.html create mode 100644 docs/types/_mdf.js_core.Layer.Observable.html create mode 100644 docs/types/_mdf.js_core.Layer.Provider.ProviderState.html create mode 100644 docs/types/_mdf.js_crash.Cause.html create mode 100644 docs/types/_mdf.js_crash.ContextLink.html create mode 100644 docs/types/_mdf.js_crash.Links.html create mode 100644 docs/types/_mdf.js_crash.SimpleLink.html create mode 100644 docs/types/_mdf.js_doorkeeper.ResultCallback.html create mode 100644 docs/types/_mdf.js_doorkeeper.SchemaSelector.html create mode 100644 docs/types/_mdf.js_doorkeeper.ValidatedOutput.html create mode 100644 docs/types/_mdf.js_elastic-provider.Elastic.Provider.html create mode 100644 docs/types/_mdf.js_elastic-provider._internal_.Status.html create mode 100644 docs/types/_mdf.js_faker.Builder.html create mode 100644 docs/types/_mdf.js_faker.DefaultValue.html create mode 100644 docs/types/_mdf.js_faker.Dependencies.html create mode 100644 docs/types/_mdf.js_faker._internal_.GeneratorOptions.html create mode 100644 docs/types/_mdf.js_file-flinger._internal_.InternalKeygenOptions.html create mode 100644 docs/types/_mdf.js_file-flinger._internal_.InternalWatcherOptions.html create mode 100644 docs/types/_mdf.js_file-flinger._internal_.MetricInstances.html create mode 100644 docs/types/_mdf.js_firehose.JobEventHandler.html create mode 100644 docs/types/_mdf.js_firehose.Plugs.Sink.Any.html create mode 100644 docs/types/_mdf.js_firehose.Plugs.Source.Any.html create mode 100644 docs/types/_mdf.js_firehose._internal_.MetricInstances.html create mode 100644 docs/types/_mdf.js_firehose._internal_.OpenJobHandler.html create mode 100644 docs/types/_mdf.js_firehose._internal_.OpenJobObject.html create mode 100644 docs/types/_mdf.js_firehose._internal_.OpenJobRequest.html rename docs/types/{_mdf_js_openc2_core.Control.AllowedResultPropertyTypes.html => _mdf.js_firehose._internal_.OpenStrategy.html} (50%) create mode 100644 docs/types/_mdf.js_firehose._internal_.Sinks.html create mode 100644 docs/types/_mdf.js_firehose._internal_.Sources.html create mode 100644 docs/types/_mdf.js_http-client-provider.HTTP.Provider.html create mode 100644 docs/types/_mdf.js_http-server-provider.HTTP.Provider.html create mode 100644 docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Config.html create mode 100644 docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Provider.html create mode 100644 docs/types/_mdf.js_kafka-provider.Consumer.Provider.html create mode 100644 docs/types/_mdf.js_kafka-provider.Producer.Provider.html create mode 100644 docs/types/_mdf.js_kafka-provider._internal_.SystemStatus.html create mode 100644 docs/types/_mdf.js_logger.FluentdTransportConfig.html create mode 100644 docs/types/_mdf.js_logger.LogLevel.html create mode 100644 docs/types/_mdf.js_logger.LoggerFunction.html create mode 100644 docs/types/_mdf.js_middlewares.AfterRoutesMiddlewares.html create mode 100644 docs/types/_mdf.js_middlewares.AuditCategory.html create mode 100644 docs/types/_mdf.js_middlewares.AuthZOptions.html create mode 100644 docs/types/_mdf.js_middlewares.BeforeRoutesMiddlewares.html create mode 100644 docs/types/_mdf.js_middlewares.EndpointsMiddlewares.html create mode 100644 docs/types/_mdf.js_middlewares.Middlewares.html create mode 100644 docs/types/_mdf.js_middlewares._internal_.CacheEntry.html create mode 100644 docs/types/_mdf.js_middlewares._internal_.MetricInstances.html create mode 100644 docs/types/_mdf.js_middlewares._internal_.OutgoingHttpHeaderValue.html create mode 100644 docs/types/_mdf.js_mongo-provider.Mongo.Provider.html create mode 100644 docs/types/_mdf.js_mqtt-provider.MQTT.Provider.html create mode 100644 docs/types/_mdf.js_openc2-core.CommandJobDone.html create mode 100644 docs/types/_mdf.js_openc2-core.CommandJobHandler.html create mode 100644 docs/types/_mdf.js_openc2-core.Control.ActionTargetPairs.html create mode 100644 docs/types/_mdf.js_openc2-core.Control.ActionType.html create mode 100644 docs/types/_mdf.js_openc2-core.Control.AllowedResultPropertyTypes.html create mode 100644 docs/types/_mdf.js_openc2-core.Control.Message.html create mode 100644 docs/types/_mdf.js_openc2-core.Control.Namespace.html create mode 100644 docs/types/_mdf.js_openc2-core.OnCommandHandler.html create mode 100644 docs/types/_mdf.js_openc2-core.Resolver.html create mode 100644 docs/types/_mdf.js_openc2-core.ResolverEntry.html create mode 100644 docs/types/_mdf.js_openc2-core.ResolverMap.html create mode 100644 docs/types/_mdf.js_openc2-core._internal_.CommandResponse.html create mode 100644 docs/types/_mdf.js_openc2-core._internal_.CommandResponseHandler.html create mode 100644 docs/types/_mdf.js_openc2.Adapters.Dummy.Config.html create mode 100644 docs/types/_mdf.js_openc2.Adapters.Redis.Config.html create mode 100644 docs/types/_mdf.js_openc2.Adapters.SocketIO.Config.html create mode 100644 docs/types/_mdf.js_openc2.RedisClientOptions.html create mode 100644 docs/types/_mdf.js_openc2.SocketIOClientOptions.html create mode 100644 docs/types/_mdf.js_openc2.SocketIOServerOptions.html create mode 100644 docs/types/_mdf.js_redis-provider.Redis.Config.html create mode 100644 docs/types/_mdf.js_redis-provider.Redis.Provider.html create mode 100644 docs/types/_mdf.js_redis-provider._internal_.ProcessesSupervised.html create mode 100644 docs/types/_mdf.js_s3-provider.S3.Provider.html create mode 100644 docs/types/_mdf.js_service-registry.ConsumerAdapterOptions.html create mode 100644 docs/types/_mdf.js_service-registry.CustomSetting.html create mode 100644 docs/types/_mdf.js_service-registry.CustomSettings.html create mode 100644 docs/types/_mdf.js_service-registry.ErrorRecord.html create mode 100644 docs/types/_mdf.js_service-registry._internal_.HandleableError.html create mode 100644 docs/types/_mdf.js_service-setup-provider.Setup.Provider.html create mode 100644 docs/types/_mdf.js_service-setup-provider._internal_.FileEntry.html create mode 100644 docs/types/_mdf.js_socket-client-provider.SocketIOClient.Provider.html create mode 100644 docs/types/_mdf.js_socket-server-provider.SocketIOServer.Provider.html create mode 100644 docs/types/_mdf.js_tasks.DefaultPollingGroups.html create mode 100644 docs/types/_mdf.js_tasks.DoneListener.html create mode 100644 docs/types/_mdf.js_tasks.MetricsDefinitions.html create mode 100644 docs/types/_mdf.js_tasks.PollingGroup.html create mode 100644 docs/types/_mdf.js_tasks.RetryStrategy.html create mode 100644 docs/types/_mdf.js_tasks.ScanMetricsDefinitions.html create mode 100644 docs/types/_mdf.js_tasks.Strategy-1.html create mode 100644 docs/types/_mdf.js_tasks.TaskBaseConfig.html create mode 100644 docs/types/_mdf.js_tasks.TaskMetricsDefinitions.html create mode 100644 docs/types/_mdf.js_tasks.TaskState.html create mode 100644 docs/types/_mdf.js_utils.Coercible.html create mode 100644 docs/types/_mdf.js_utils.Format.html create mode 100644 docs/types/_mdf.js_utils.FormatFunction.html create mode 100644 docs/types/_mdf.js_utils.LoggerFunction.html create mode 100644 docs/types/_mdf.js_utils.LoggerInstance.html create mode 100644 docs/types/_mdf.js_utils.TaskArguments.html create mode 100644 docs/types/_mdf.js_utils.TaskAsPromise.html delete mode 100644 docs/types/_mdf_js_amqp_provider.Receiver.Provider.html delete mode 100644 docs/types/_mdf_js_amqp_provider.Sender.Provider.html delete mode 100644 docs/types/_mdf_js_core.Health.CheckEntry.html delete mode 100644 docs/types/_mdf_js_core.Health.Checks.html delete mode 100644 docs/types/_mdf_js_core.Health.ComponentName.html delete mode 100644 docs/types/_mdf_js_core.Health.MeasurementName.html delete mode 100644 docs/types/_mdf_js_core.Health.Status-1.html delete mode 100644 docs/types/_mdf_js_core.Jobs.AnyHeaders.html delete mode 100644 docs/types/_mdf_js_core.Jobs.AnyOptions.html delete mode 100644 docs/types/_mdf_js_core.Jobs.DoneEventHandler.html delete mode 100644 docs/types/_mdf_js_core.Jobs.Headers.html delete mode 100644 docs/types/_mdf_js_core.Jobs.Options.html delete mode 100644 docs/types/_mdf_js_core.Layer.Observable.html delete mode 100644 docs/types/_mdf_js_core.Layer.Provider.ProviderState.html delete mode 100644 docs/types/_mdf_js_crash.Cause.html delete mode 100644 docs/types/_mdf_js_crash.ContextLink.html delete mode 100644 docs/types/_mdf_js_crash.Links.html delete mode 100644 docs/types/_mdf_js_crash.SimpleLink.html delete mode 100644 docs/types/_mdf_js_doorkeeper.ResultCallback.html delete mode 100644 docs/types/_mdf_js_doorkeeper.SchemaSelector.html delete mode 100644 docs/types/_mdf_js_doorkeeper.ValidatedOutput.html delete mode 100644 docs/types/_mdf_js_elastic_provider.Elastic.Provider.html delete mode 100644 docs/types/_mdf_js_faker.Builder.html delete mode 100644 docs/types/_mdf_js_faker.DefaultValue.html delete mode 100644 docs/types/_mdf_js_faker.Dependencies.html delete mode 100644 docs/types/_mdf_js_firehose.JobEventHandler.html delete mode 100644 docs/types/_mdf_js_firehose.Plugs.Sink.Any.html delete mode 100644 docs/types/_mdf_js_firehose.Plugs.Sink.Tap.html delete mode 100644 docs/types/_mdf_js_firehose.Plugs.Source.Any.html delete mode 100644 docs/types/_mdf_js_http_client_provider.HTTP.Provider.html delete mode 100644 docs/types/_mdf_js_http_server_provider.HTTP.Provider.html delete mode 100644 docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Config.html delete mode 100644 docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Provider.html delete mode 100644 docs/types/_mdf_js_kafka_provider.Consumer.Provider.html delete mode 100644 docs/types/_mdf_js_kafka_provider.Producer.Provider.html delete mode 100644 docs/types/_mdf_js_logger.FluentdTransportConfig.html delete mode 100644 docs/types/_mdf_js_middlewares.AfterRoutesMiddlewares.html delete mode 100644 docs/types/_mdf_js_middlewares.AuditCategory.html delete mode 100644 docs/types/_mdf_js_middlewares.BeforeRoutesMiddlewares.html delete mode 100644 docs/types/_mdf_js_middlewares.EndpointsMiddlewares.html delete mode 100644 docs/types/_mdf_js_middlewares.Middlewares.html delete mode 100644 docs/types/_mdf_js_mongo_provider.Mongo.Provider.html delete mode 100644 docs/types/_mdf_js_mqtt_provider.MQTT.Provider.html delete mode 100644 docs/types/_mdf_js_openc2.Adapters.Dummy.Config.html delete mode 100644 docs/types/_mdf_js_openc2.Adapters.Redis.Config.html delete mode 100644 docs/types/_mdf_js_openc2.Adapters.SocketIO.Config.html delete mode 100644 docs/types/_mdf_js_openc2_core.CommandJobDone.html delete mode 100644 docs/types/_mdf_js_openc2_core.CommandJobHandler.html delete mode 100644 docs/types/_mdf_js_openc2_core.Control.ActionTargetPairs.html delete mode 100644 docs/types/_mdf_js_openc2_core.Control.ActionType.html delete mode 100644 docs/types/_mdf_js_openc2_core.Control.Message.html delete mode 100644 docs/types/_mdf_js_openc2_core.Control.Namespace.html delete mode 100644 docs/types/_mdf_js_openc2_core.OnCommandHandler.html delete mode 100644 docs/types/_mdf_js_openc2_core.Resolver.html delete mode 100644 docs/types/_mdf_js_openc2_core.ResolverEntry.html delete mode 100644 docs/types/_mdf_js_openc2_core.ResolverMap.html delete mode 100644 docs/types/_mdf_js_redis_provider.Redis.Config.html delete mode 100644 docs/types/_mdf_js_redis_provider.Redis.Provider.html delete mode 100644 docs/types/_mdf_js_s3_provider.S3.Provider.html delete mode 100644 docs/types/_mdf_js_service_registry.ConsumerAdapterOptions.html delete mode 100644 docs/types/_mdf_js_service_registry.CustomSetting.html delete mode 100644 docs/types/_mdf_js_service_registry.CustomSettings.html delete mode 100644 docs/types/_mdf_js_service_registry.ErrorRecord.html delete mode 100644 docs/types/_mdf_js_service_setup_provider.Setup.Provider.html delete mode 100644 docs/types/_mdf_js_socket_client_provider.SocketIOClient.Provider.html delete mode 100644 docs/types/_mdf_js_socket_server_provider.SocketIOServer.Provider.html delete mode 100644 docs/types/_mdf_js_tasks.DefaultPollingGroups.html delete mode 100644 docs/types/_mdf_js_tasks.DoneListener.html delete mode 100644 docs/types/_mdf_js_tasks.MetricsDefinitions.html delete mode 100644 docs/types/_mdf_js_tasks.PollingGroup.html delete mode 100644 docs/types/_mdf_js_tasks.RetryStrategy.html delete mode 100644 docs/types/_mdf_js_tasks.Strategy-1.html delete mode 100644 docs/types/_mdf_js_tasks.TaskBaseConfig.html delete mode 100644 docs/types/_mdf_js_tasks.TaskState.html delete mode 100644 docs/types/_mdf_js_utils.LoggerFunction.html delete mode 100644 docs/types/_mdf_js_utils.TaskArguments.html delete mode 100644 docs/types/_mdf_js_utils.TaskAsPromise.html create mode 100644 docs/variables/_mdf.js_amqp-provider.Receiver.Factory.html create mode 100644 docs/variables/_mdf.js_amqp-provider.Sender.Factory.html create mode 100644 docs/variables/_mdf.js_core.Health.STATUSES.html create mode 100644 docs/variables/_mdf.js_core.Layer.Provider.PROVIDER_STATES.html create mode 100644 docs/variables/_mdf.js_elastic-provider.Elastic.Factory.html create mode 100644 docs/variables/_mdf.js_http-client-provider.HTTP.Factory.html create mode 100644 docs/variables/_mdf.js_http-server-provider.HTTP.Factory.html create mode 100644 docs/variables/_mdf.js_jsonl-archiver.JSONLArchiver.Factory.html create mode 100644 docs/variables/_mdf.js_kafka-provider.Consumer.Factory.html create mode 100644 docs/variables/_mdf.js_kafka-provider.Producer.Factory.html create mode 100644 docs/variables/_mdf.js_logger.LOG_LEVELS.html create mode 100644 docs/variables/_mdf.js_logger.default.html create mode 100644 docs/variables/_mdf.js_middlewares.Middleware.html create mode 100644 docs/variables/_mdf.js_mongo-provider.Mongo.Factory.html create mode 100644 docs/variables/_mdf.js_mqtt-provider.MQTT.Factory.html create mode 100644 docs/variables/_mdf.js_openc2-core.Control.ACTION_TYPES.html create mode 100644 docs/variables/_mdf.js_redis-provider.Redis.Factory.html create mode 100644 docs/variables/_mdf.js_s3-provider.S3.Factory.html create mode 100644 docs/variables/_mdf.js_service-setup-provider.Setup.Factory.html create mode 100644 docs/variables/_mdf.js_socket-client-provider.SocketIOClient.Factory.html create mode 100644 docs/variables/_mdf.js_socket-server-provider.SocketIOServer.Factory.html create mode 100644 docs/variables/_mdf.js_tasks.STRATEGIES.html create mode 100644 docs/variables/_mdf.js_tasks.TASK_STATES.html create mode 100644 docs/variables/_mdf.js_utils.MAX_WAIT_TIME.html create mode 100644 docs/variables/_mdf.js_utils.WAIT_TIME.html delete mode 100644 docs/variables/_mdf_js_amqp_provider.Receiver.Factory.html delete mode 100644 docs/variables/_mdf_js_amqp_provider.Sender.Factory.html delete mode 100644 docs/variables/_mdf_js_core.Health.STATUSES.html delete mode 100644 docs/variables/_mdf_js_core.Layer.Provider.PROVIDER_STATES.html delete mode 100644 docs/variables/_mdf_js_elastic_provider.Elastic.Factory.html delete mode 100644 docs/variables/_mdf_js_http_client_provider.HTTP.Factory.html delete mode 100644 docs/variables/_mdf_js_http_server_provider.HTTP.Factory.html delete mode 100644 docs/variables/_mdf_js_jsonl_archiver.JSONLArchiver.Factory.html delete mode 100644 docs/variables/_mdf_js_kafka_provider.Consumer.Factory.html delete mode 100644 docs/variables/_mdf_js_kafka_provider.Producer.Factory.html delete mode 100644 docs/variables/_mdf_js_logger.default.html delete mode 100644 docs/variables/_mdf_js_middlewares.Middleware.html delete mode 100644 docs/variables/_mdf_js_mongo_provider.Mongo.Factory.html delete mode 100644 docs/variables/_mdf_js_mqtt_provider.MQTT.Factory.html delete mode 100644 docs/variables/_mdf_js_openc2_core.Control.ACTION_TYPES.html delete mode 100644 docs/variables/_mdf_js_redis_provider.Redis.Factory.html delete mode 100644 docs/variables/_mdf_js_s3_provider.S3.Factory.html delete mode 100644 docs/variables/_mdf_js_service_setup_provider.Setup.Factory.html delete mode 100644 docs/variables/_mdf_js_socket_client_provider.SocketIOClient.Factory.html delete mode 100644 docs/variables/_mdf_js_socket_server_provider.SocketIOServer.Factory.html delete mode 100644 docs/variables/_mdf_js_tasks.STRATEGIES.html delete mode 100644 docs/variables/_mdf_js_tasks.TASK_STATES.html delete mode 100644 docs/variables/_mdf_js_utils.MAX_WAIT_TIME.html delete mode 100644 docs/variables/_mdf_js_utils.WAIT_TIME.html create mode 100644 packages/api/file-flinger/.eslintignore create mode 100644 packages/api/file-flinger/.eslintrc.js create mode 100644 packages/api/file-flinger/README.md create mode 100644 packages/api/file-flinger/jest.config.js create mode 100644 packages/api/file-flinger/package.json create mode 100644 packages/api/file-flinger/src/FileFinger.test.ts create mode 100644 packages/api/file-flinger/src/FileFlinger.ts create mode 100644 packages/api/file-flinger/src/engine/Engine.test.ts create mode 100644 packages/api/file-flinger/src/engine/Engine.ts create mode 100644 packages/api/file-flinger/src/engine/FileTasks.test.ts create mode 100644 packages/api/file-flinger/src/engine/FileTasks.ts create mode 100644 packages/api/file-flinger/src/engine/index.ts create mode 100644 packages/api/file-flinger/src/engine/types/EngineOptions.i.ts create mode 100644 packages/api/file-flinger/src/engine/types/ErrorStrategy.t..ts create mode 100644 packages/api/file-flinger/src/engine/types/ErroredFile.i.ts create mode 100644 packages/api/file-flinger/src/engine/types/FileTaskIdentifiers.t.ts create mode 100644 packages/api/file-flinger/src/engine/types/FileTasksOptions.i.ts create mode 100644 packages/api/file-flinger/src/engine/types/PostProcessingStrategy.t.ts create mode 100644 packages/api/file-flinger/src/engine/types/const.ts create mode 100644 packages/api/file-flinger/src/engine/types/index.ts create mode 100644 packages/api/file-flinger/src/index.ts create mode 100644 packages/api/file-flinger/src/keygen/Keygen.test.ts create mode 100644 packages/api/file-flinger/src/keygen/Keygen.ts create mode 100644 packages/api/file-flinger/src/keygen/index.ts create mode 100644 packages/api/file-flinger/src/keygen/types/KeygenOptions.i.ts create mode 100644 packages/api/file-flinger/src/keygen/types/const.ts create mode 100644 packages/api/file-flinger/src/keygen/types/index.ts create mode 100644 packages/api/file-flinger/src/metrics/MetricsHandler.ts create mode 100644 packages/api/file-flinger/src/metrics/index.ts create mode 100644 packages/api/file-flinger/src/pusher/PusherWrapper.test.ts create mode 100644 packages/api/file-flinger/src/pusher/PusherWrapper.ts create mode 100644 packages/api/file-flinger/src/pusher/index.ts create mode 100644 packages/api/file-flinger/src/pusher/types/Pusher.i.ts create mode 100644 packages/api/file-flinger/src/pusher/types/index.ts create mode 100644 packages/api/file-flinger/src/types/FileFlingerOptions.i.ts create mode 100644 packages/api/file-flinger/src/types/const.ts create mode 100644 packages/api/file-flinger/src/types/index.ts create mode 100644 packages/api/file-flinger/src/watcher/Watcher.test.ts create mode 100644 packages/api/file-flinger/src/watcher/Watcher.ts create mode 100644 packages/api/file-flinger/src/watcher/index.ts create mode 100644 packages/api/file-flinger/src/watcher/types/WatcherOptions.i.ts create mode 100644 packages/api/file-flinger/src/watcher/types/const.ts create mode 100644 packages/api/file-flinger/src/watcher/types/index.ts create mode 100644 packages/api/file-flinger/stryker.conf.js create mode 100644 packages/api/file-flinger/tsconfig.build.json create mode 100644 packages/api/file-flinger/tsconfig.json create mode 100644 packages/api/file-flinger/tsconfig.lint.json create mode 100644 packages/api/file-flinger/tsconfig.spec.json create mode 100644 packages/api/file-flinger/typedoc.json create mode 100644 packages/api/firehose/media/firehose-diagram.svg create mode 100644 packages/api/firehose/src/types/JobEventHandler.t.ts create mode 100644 packages/api/logger/media/logging-capture.png delete mode 100644 test.mjs diff --git a/.config/azure-pipelines.yml b/.config/azure-pipelines.yml index 3ee64bf8..e2d22686 100644 --- a/.config/azure-pipelines.yml +++ b/.config/azure-pipelines.yml @@ -17,11 +17,11 @@ variables: value: 'mdf-js-api' ## Node version - name: nodeVersion - value: '20' + value: '22' ## Releases Wiki page - name: artifactWikiFileName value: '@mdf.js%2Djs-%2D-API' - ## GitFlow variables for branch maching + ## GitFlow variables for branch matching - template: mdf-pipelines-variables.yml stages: - stage: 'APP' diff --git a/.config/envDoc.mjs b/.config/envDoc.mjs index bdc254ad..7f89c853 100644 --- a/.config/envDoc.mjs +++ b/.config/envDoc.mjs @@ -36,9 +36,10 @@ const envVarRegex = /process\.env\[['"](?[^'"]+)['"]\]/; const defaultValueRegex = /(default:|@defaultValue) (?[^,]+)/; const srcFiles = glob.sync(SOURCE_FILES, { - nodir: true, ignore: { + nodir: true, + ignore: { ignored: p => /\.test.ts$/.test(p.name), - } + }, }); const variables = []; @@ -102,7 +103,7 @@ function findAllEnvironmentVariablesInFile(fileContent, path) { comment = comment.replace(defaultValueRegex, '').trim(); } variables.push({ name: matches.groups['varName'], comment, path, defaultValue }); - } catch (error) { + } catch (error) { console.log(`ERROR: Error while processing file ${path}: ${error.message}`); } } @@ -159,7 +160,9 @@ function findUpperCommentInFile(lines, index) { } else { const firstLine = lines[startOfComment].replace('/**', ''); const lastLine = lines[endOfComment].replace('*/', ''); - const middleLines = lines.slice(startOfComment + 1, endOfComment).map(line => line.replace('*', '')); + const middleLines = lines + .slice(startOfComment + 1, endOfComment) + .map(line => line.replace('*', '')); comment = [firstLine, ...middleLines, lastLine].join(' '); } // Clean spaces if there are more than one space between words or at the beginning or end of the comment @@ -191,7 +194,7 @@ function isConstantDeclaration(line) { } /** * Finds and removes the section containing environment variables from a given Markdown document. - * + * * @param {object} readMeMD - The Markdown document object. */ function findAndRemoveEnvironmentVariablesSection(readMeMD) { @@ -220,7 +223,7 @@ function findAndRemoveEnvironmentVariablesSection(readMeMD) { } /** * Finds, removes, and returns the license section from the given readMeMD object. - * + * * @param {Object} readMeMD - The readMeMD object. * @returns {Array} - An array of nodes representing the license section. */ @@ -268,7 +271,7 @@ function deletePositionPropertyRecursively(nodes) { } /** * Generates a table of environment variables. - * + * * @param {Array} envVariables - An array of environment variables. * @returns {Array} - An array representing the table of environment variables. */ @@ -306,7 +309,7 @@ function generateTableOfEnvironmentVariables(envVariables) { } /** * Generates a list of environment variables. - * + * * @param {Array} envVariables - The array of environment variables. * @returns {Array} - The list of environment variables in a specific format. */ @@ -324,14 +327,17 @@ function generateListOfEnvironmentVariables(envVariables) { }; for (const envVariable of envVariables) { if (!envVariable.comment) { - console.log(`WARNING: No comment found for environment variable ${envVariable.name} in file ${envVariable.path}`); - }; + console.log( + `WARNING: No comment found for environment variable ${envVariable.name} in file ${envVariable.path}` + ); + } let defaultValue = []; if (envVariable.defaultValue) { defaultValue = [ - { type: 'text', value: ' (default: ', }, + { type: 'text', value: ' (default: ' }, { type: 'inlineCode', value: envVariable.defaultValue }, - { type: 'text', value: `): ${envVariable.comment}` }]; + { type: 'text', value: `): ${envVariable.comment}` }, + ]; } else { defaultValue = [{ type: 'text', value: `: ${envVariable.comment}` }]; } @@ -339,18 +345,20 @@ function generateListOfEnvironmentVariables(envVariables) { type: 'listItem', spread: false, checked: null, - children: [{ - type: 'paragraph', - children: [ - { - type: 'strong', children: [ - { type: 'text', value: envVariable.name }, - ] - }, - ...defaultValue, - ], - }], + children: [ + { + type: 'paragraph', + children: [ + { + type: 'strong', + children: [{ type: 'text', value: envVariable.name }], + }, + ...defaultValue, + ], + }, + ], }); } return [envVariablesTitle, envVariablesList]; } + diff --git a/.config/mdf-publish-artifacts-lerna.yml b/.config/mdf-publish-artifacts-lerna.yml index bf7bec07..8604608e 100644 --- a/.config/mdf-publish-artifacts-lerna.yml +++ b/.config/mdf-publish-artifacts-lerna.yml @@ -25,7 +25,7 @@ steps: - task: Yarn@3 displayName: Publish package to internal feed as alfa version - condition: and(succeeded(), eq(variables.isDevelop, true)) + condition: and(succeeded(), or(eq(variables.isDevelop, true), eq(variables.isFeature, true))) inputs: projectDirectory: '.' arguments: 'lerna publish $(Build.BuildNumber) --amend --dist-tag alpha --loglevel silly -m "chore(release): publish %v" --yes' diff --git a/.config/mdf-test-sonarqube-analysis.yml b/.config/mdf-test-sonarqube-analysis.yml index 54a88d07..c5504e22 100644 --- a/.config/mdf-test-sonarqube-analysis.yml +++ b/.config/mdf-test-sonarqube-analysis.yml @@ -1,22 +1,22 @@ steps: -- task: SonarQubePrepare@6 +- task: SonarQubePrepare@7 displayName: 'Setting Sonarqube analysis' inputs: SonarQube: 'NetinSystems-SonarQube Endpoint-MytraManagementSystem' scannerMode: 'CLI' - cliVersion: '6.1.0.4477' + cliVersion: '6.2.1.4610' configMode: 'file' configFile: './.config/sonar-project.properties' projectVersion: $(Build.BuildNumber) extraProperties: sonar.projectVersion=$(Build.BuildNumber) -- task: SonarQubeAnalyze@6 +- task: SonarQubeAnalyze@7 displayName: 'Run Code Analysis' inputs: jdkversion: 'JAVA_HOME_17_X64' -- task: SonarQubePublish@6 +- task: SonarQubePublish@7 displayName: 'Publish Quality Gate Result' inputs: pollingTimeoutSec: '300' diff --git a/.config/sonar-project.properties b/.config/sonar-project.properties index 602e078a..0b4535a9 100644 --- a/.config/sonar-project.properties +++ b/.config/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey=Mytra-Development-Framework-NDF-TypeScript sonar.projectName=Mytra Development Framework - NDF - TypeScript -sonar.modules=providers-socket-server,providers-socket-client,providers-service-setup,providers-redis,providers-mqtt,providers-mongo,providers-kafka,providers-http-server,providers-http-client,providers-elastic,providers-amqp,components-service-registry,components-openc2,api-utils,api-tasks,api-openc2-core,api-middlewares,api-logger,api-firehose,api-faker,api-doorkeeper,api-crash,api-core +sonar.modules=providers-socket-server,providers-socket-client,providers-service-setup,providers-redis,providers-mqtt,providers-mongo,providers-kafka,providers-http-server,providers-http-client,providers-elastic,providers-amqp,components-service-registry,components-openc2,api-utils,api-tasks,api-openc2-core,api-middlewares,api-logger,api-firehose,api-file-flinger,api-faker,api-doorkeeper,api-crash,api-core sonar.exclusions=coveragereport/**/*,packages/**/*.test.ts,packages/**/test/*.ts,packages/**/*.js providers-socket-server.sonar.projectName=NDS-PROVIDERS-SOCKET-SERVER-Typescript @@ -12,9 +12,9 @@ providers-socket-server.sonar.test.inclusions=src/**/*.test.ts providers-socket-server.sonar.javascript.lcov.reportPaths=../../../coverage/providers/socket-server/lcov.info providers-socket-server.sonar.junit.reportPaths=../../../coverage/providers/socket-server/test-results.xml providers-socket-server.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-socket-server.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/socket-server/dependency-check-report.html providers-socket-server.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/socket-server/dependency-check-report.json + providers-socket-client.sonar.projectName=NDS-PROVIDERS-SOCKET-CLIENT-Typescript providers-socket-client.sonar.projectBaseDir=packages/providers/socket-client providers-socket-client.sonar.sources=src @@ -24,9 +24,9 @@ providers-socket-client.sonar.test.inclusions=src/**/*.test.ts providers-socket-client.sonar.javascript.lcov.reportPaths=../../../coverage/providers/socket-client/lcov.info providers-socket-client.sonar.junit.reportPaths=../../../coverage/providers/socket-client/test-results.xml providers-socket-client.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-socket-client.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/socket-client/dependency-check-report.html providers-socket-client.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/socket-client/dependency-check-report.json + providers-service-setup.sonar.projectName=NDS-PROVIDERS-SERVICE-SETUP-Typescript providers-service-setup.sonar.projectBaseDir=packages/providers/service-setup providers-service-setup.sonar.sources=src @@ -36,9 +36,9 @@ providers-service-setup.sonar.test.inclusions=src/**/*.test.ts providers-service-setup.sonar.javascript.lcov.reportPaths=../../../coverage/providers/service-setup/lcov.info providers-service-setup.sonar.junit.reportPaths=../../../coverage/providers/service-setup/test-results.xml providers-service-setup.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-service-setup.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/service-setup/dependency-check-report.html providers-service-setup.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/service-setup/dependency-check-report.json + providers-redis.sonar.projectName=NDS-PROVIDERS-REDIS-Typescript providers-redis.sonar.projectBaseDir=packages/providers/redis providers-redis.sonar.sources=src @@ -48,9 +48,9 @@ providers-redis.sonar.test.inclusions=src/**/*.test.ts providers-redis.sonar.javascript.lcov.reportPaths=../../../coverage/providers/redis/lcov.info providers-redis.sonar.junit.reportPaths=../../../coverage/providers/redis/test-results.xml providers-redis.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-redis.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/redis/dependency-check-report.html providers-redis.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/redis/dependency-check-report.json + providers-mqtt.sonar.projectName=NDS-PROVIDERS-MQTT-Typescript providers-mqtt.sonar.projectBaseDir=packages/providers/mqtt providers-mqtt.sonar.sources=src @@ -60,9 +60,9 @@ providers-mqtt.sonar.test.inclusions=src/**/*.test.ts providers-mqtt.sonar.javascript.lcov.reportPaths=../../../coverage/providers/mqtt/lcov.info providers-mqtt.sonar.junit.reportPaths=../../../coverage/providers/mqtt/test-results.xml providers-mqtt.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-mqtt.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/mqtt/dependency-check-report.html providers-mqtt.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/mqtt/dependency-check-report.json + providers-mongo.sonar.projectName=NDS-PROVIDERS-MONGO-Typescript providers-mongo.sonar.projectBaseDir=packages/providers/mongo providers-mongo.sonar.sources=src @@ -72,9 +72,9 @@ providers-mongo.sonar.test.inclusions=src/**/*.test.ts providers-mongo.sonar.javascript.lcov.reportPaths=../../../coverage/providers/mongo/lcov.info providers-mongo.sonar.junit.reportPaths=../../../coverage/providers/mongo/test-results.xml providers-mongo.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-mongo.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/mongo/dependency-check-report.html providers-mongo.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/mongo/dependency-check-report.json + providers-kafka.sonar.projectName=NDS-PROVIDERS-KAFKA-Typescript providers-kafka.sonar.projectBaseDir=packages/providers/kafka providers-kafka.sonar.sources=src @@ -84,9 +84,9 @@ providers-kafka.sonar.test.inclusions=src/**/*.test.ts providers-kafka.sonar.javascript.lcov.reportPaths=../../../coverage/providers/kafka/lcov.info providers-kafka.sonar.junit.reportPaths=../../../coverage/providers/kafka/test-results.xml providers-kafka.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-kafka.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/kafka/dependency-check-report.html providers-kafka.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/kafka/dependency-check-report.json + providers-http-server.sonar.projectName=NDS-PROVIDERS-HTTP-SERVER-Typescript providers-http-server.sonar.projectBaseDir=packages/providers/http-server providers-http-server.sonar.sources=src @@ -96,9 +96,9 @@ providers-http-server.sonar.test.inclusions=src/**/*.test.ts providers-http-server.sonar.javascript.lcov.reportPaths=../../../coverage/providers/http-server/lcov.info providers-http-server.sonar.junit.reportPaths=../../../coverage/providers/http-server/test-results.xml providers-http-server.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-http-server.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/http-server/dependency-check-report.html providers-http-server.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/http-server/dependency-check-report.json + providers-http-client.sonar.projectName=NDS-PROVIDERS-HTTP-CLIENT-Typescript providers-http-client.sonar.projectBaseDir=packages/providers/http-client providers-http-client.sonar.sources=src @@ -108,9 +108,9 @@ providers-http-client.sonar.test.inclusions=src/**/*.test.ts providers-http-client.sonar.javascript.lcov.reportPaths=../../../coverage/providers/http-client/lcov.info providers-http-client.sonar.junit.reportPaths=../../../coverage/providers/http-client/test-results.xml providers-http-client.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-http-client.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/http-client/dependency-check-report.html providers-http-client.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/http-client/dependency-check-report.json + providers-elastic.sonar.projectName=NDS-PROVIDERS-ELASTIC-Typescript providers-elastic.sonar.projectBaseDir=packages/providers/elastic providers-elastic.sonar.sources=src @@ -120,9 +120,9 @@ providers-elastic.sonar.test.inclusions=src/**/*.test.ts providers-elastic.sonar.javascript.lcov.reportPaths=../../../coverage/providers/elastic/lcov.info providers-elastic.sonar.junit.reportPaths=../../../coverage/providers/elastic/test-results.xml providers-elastic.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-elastic.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/elastic/dependency-check-report.html providers-elastic.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/elastic/dependency-check-report.json + providers-amqp.sonar.projectName=NDS-PROVIDERS-AMQP-Typescript providers-amqp.sonar.projectBaseDir=packages/providers/amqp providers-amqp.sonar.sources=src @@ -132,9 +132,9 @@ providers-amqp.sonar.test.inclusions=src/**/*.test.ts providers-amqp.sonar.javascript.lcov.reportPaths=../../../coverage/providers/amqp/lcov.info providers-amqp.sonar.junit.reportPaths=../../../coverage/providers/amqp/test-results.xml providers-amqp.sonar.typescript.tsconfigPath=tsconfig.build.json - providers-amqp.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/providers/amqp/dependency-check-report.html providers-amqp.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/providers/amqp/dependency-check-report.json + components-service-registry.sonar.projectName=NDS-COMPONENTS-SERVICE-REGISTRY-Typescript components-service-registry.sonar.projectBaseDir=packages/components/service-registry components-service-registry.sonar.sources=src @@ -144,9 +144,9 @@ components-service-registry.sonar.test.inclusions=src/**/*.test.ts components-service-registry.sonar.javascript.lcov.reportPaths=../../../coverage/components/service-registry/lcov.info components-service-registry.sonar.junit.reportPaths=../../../coverage/components/service-registry/test-results.xml components-service-registry.sonar.typescript.tsconfigPath=tsconfig.build.json - components-service-registry.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/components/service-registry/dependency-check-report.html components-service-registry.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/components/service-registry/dependency-check-report.json + components-openc2.sonar.projectName=NDS-COMPONENTS-OPENC2-Typescript components-openc2.sonar.projectBaseDir=packages/components/openc2 components-openc2.sonar.sources=src @@ -156,9 +156,9 @@ components-openc2.sonar.test.inclusions=src/**/*.test.ts components-openc2.sonar.javascript.lcov.reportPaths=../../../coverage/components/openc2/lcov.info components-openc2.sonar.junit.reportPaths=../../../coverage/components/openc2/test-results.xml components-openc2.sonar.typescript.tsconfigPath=tsconfig.build.json - components-openc2.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/components/openc2/dependency-check-report.html components-openc2.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/components/openc2/dependency-check-report.json + api-utils.sonar.projectName=NDS-API-UTILS-Typescript api-utils.sonar.projectBaseDir=packages/api/utils api-utils.sonar.sources=src @@ -168,9 +168,9 @@ api-utils.sonar.test.inclusions=src/**/*.test.ts api-utils.sonar.javascript.lcov.reportPaths=../../../coverage/api/utils/lcov.info api-utils.sonar.junit.reportPaths=../../../coverage/api/utils/test-results.xml api-utils.sonar.typescript.tsconfigPath=tsconfig.build.json - api-utils.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/utils/dependency-check-report.html api-utils.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/utils/dependency-check-report.json + api-tasks.sonar.projectName=NDS-API-TASKS-Typescript api-tasks.sonar.projectBaseDir=packages/api/tasks api-tasks.sonar.sources=src @@ -180,9 +180,9 @@ api-tasks.sonar.test.inclusions=src/**/*.test.ts api-tasks.sonar.javascript.lcov.reportPaths=../../../coverage/api/tasks/lcov.info api-tasks.sonar.junit.reportPaths=../../../coverage/api/tasks/test-results.xml api-tasks.sonar.typescript.tsconfigPath=tsconfig.build.json - api-tasks.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/tasks/dependency-check-report.html api-tasks.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/tasks/dependency-check-report.json + api-openc2-core.sonar.projectName=NDS-API-OPENC2-CORE-Typescript api-openc2-core.sonar.projectBaseDir=packages/api/openc2-core api-openc2-core.sonar.sources=src @@ -192,9 +192,9 @@ api-openc2-core.sonar.test.inclusions=src/**/*.test.ts api-openc2-core.sonar.javascript.lcov.reportPaths=../../../coverage/api/openc2-core/lcov.info api-openc2-core.sonar.junit.reportPaths=../../../coverage/api/openc2-core/test-results.xml api-openc2-core.sonar.typescript.tsconfigPath=tsconfig.build.json - api-openc2-core.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/openc2-core/dependency-check-report.html api-openc2-core.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/openc2-core/dependency-check-report.json + api-middlewares.sonar.projectName=NDS-API-MIDDLEWARES-Typescript api-middlewares.sonar.projectBaseDir=packages/api/middlewares api-middlewares.sonar.sources=src @@ -204,9 +204,9 @@ api-middlewares.sonar.test.inclusions=src/**/*.test.ts api-middlewares.sonar.javascript.lcov.reportPaths=../../../coverage/api/middlewares/lcov.info api-middlewares.sonar.junit.reportPaths=../../../coverage/api/middlewares/test-results.xml api-middlewares.sonar.typescript.tsconfigPath=tsconfig.build.json - api-middlewares.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/middlewares/dependency-check-report.html api-middlewares.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/middlewares/dependency-check-report.json + api-logger.sonar.projectName=NDS-API-LOGGER-Typescript api-logger.sonar.projectBaseDir=packages/api/logger api-logger.sonar.sources=src @@ -216,9 +216,9 @@ api-logger.sonar.test.inclusions=src/**/*.test.ts api-logger.sonar.javascript.lcov.reportPaths=../../../coverage/api/logger/lcov.info api-logger.sonar.junit.reportPaths=../../../coverage/api/logger/test-results.xml api-logger.sonar.typescript.tsconfigPath=tsconfig.build.json - api-logger.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/logger/dependency-check-report.html api-logger.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/logger/dependency-check-report.json + api-firehose.sonar.projectName=NDS-API-FIREHOSE-Typescript api-firehose.sonar.projectBaseDir=packages/api/firehose api-firehose.sonar.sources=src @@ -228,9 +228,21 @@ api-firehose.sonar.test.inclusions=src/**/*.test.ts api-firehose.sonar.javascript.lcov.reportPaths=../../../coverage/api/firehose/lcov.info api-firehose.sonar.junit.reportPaths=../../../coverage/api/firehose/test-results.xml api-firehose.sonar.typescript.tsconfigPath=tsconfig.build.json - api-firehose.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/firehose/dependency-check-report.html api-firehose.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/firehose/dependency-check-report.json + +api-file-flinger.sonar.projectName=NDS-API-FILE-FLINGER-Typescript +api-file-flinger.sonar.projectBaseDir=packages/api/file-flinger +api-file-flinger.sonar.sources=src +api-file-flinger.sonar.inclusions=src/**/* +api-file-flinger.sonar.exclusions=src/**/*.test.ts,src/**/test/*.ts +api-file-flinger.sonar.test.inclusions=src/**/*.test.ts +api-file-flinger.sonar.javascript.lcov.reportPaths=../../../coverage/api/file-flinger/lcov.info +api-file-flinger.sonar.junit.reportPaths=../../../coverage/api/file-flinger/test-results.xml +api-file-flinger.sonar.typescript.tsconfigPath=tsconfig.build.json +api-file-flinger.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/file-flinger/dependency-check-report.html +api-file-flinger.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/file-flinger/dependency-check-report.json + api-faker.sonar.projectName=NDS-API-FAKER-Typescript api-faker.sonar.projectBaseDir=packages/api/faker api-faker.sonar.sources=src @@ -240,9 +252,9 @@ api-faker.sonar.test.inclusions=src/**/*.test.ts api-faker.sonar.javascript.lcov.reportPaths=../../../coverage/api/faker/lcov.info api-faker.sonar.junit.reportPaths=../../../coverage/api/faker/test-results.xml api-faker.sonar.typescript.tsconfigPath=tsconfig.build.json - api-faker.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/faker/dependency-check-report.html api-faker.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/faker/dependency-check-report.json + api-doorkeeper.sonar.projectName=NDS-API-DOORKEEPER-Typescript api-doorkeeper.sonar.projectBaseDir=packages/api/doorkeeper api-doorkeeper.sonar.sources=src @@ -252,9 +264,9 @@ api-doorkeeper.sonar.test.inclusions=src/**/*.test.ts api-doorkeeper.sonar.javascript.lcov.reportPaths=../../../coverage/api/doorkeeper/lcov.info api-doorkeeper.sonar.junit.reportPaths=../../../coverage/api/doorkeeper/test-results.xml api-doorkeeper.sonar.typescript.tsconfigPath=tsconfig.build.json - api-doorkeeper.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/doorkeeper/dependency-check-report.html api-doorkeeper.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/doorkeeper/dependency-check-report.json + api-crash.sonar.projectName=NDS-API-CRASH-Typescript api-crash.sonar.projectBaseDir=packages/api/crash api-crash.sonar.sources=src @@ -264,9 +276,9 @@ api-crash.sonar.test.inclusions=src/**/*.test.ts api-crash.sonar.javascript.lcov.reportPaths=../../../coverage/api/crash/lcov.info api-crash.sonar.junit.reportPaths=../../../coverage/api/crash/test-results.xml api-crash.sonar.typescript.tsconfigPath=tsconfig.build.json - api-crash.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/crash/dependency-check-report.html api-crash.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/crash/dependency-check-report.json + api-core.sonar.projectName=NDS-API-CORE-Typescript api-core.sonar.projectBaseDir=packages/api/core api-core.sonar.sources=src @@ -276,6 +288,5 @@ api-core.sonar.test.inclusions=src/**/*.test.ts api-core.sonar.javascript.lcov.reportPaths=../../../coverage/api/core/lcov.info api-core.sonar.junit.reportPaths=../../../coverage/api/core/test-results.xml api-core.sonar.typescript.tsconfigPath=tsconfig.build.json - api-core.sonar.dependencyCheck.htmlReportPath=../../../dependency-check-report/api/core/dependency-check-report.html api-core.sonar.dependencyCheck.jsonReportPath=../../../dependency-check-report/api/core/dependency-check-report.json diff --git a/.nvmrc b/.nvmrc index 9a2a0e21..53d1c14d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/.vscode/settings.json b/.vscode/settings.json index 5aaf7ae7..3515152c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,7 +76,10 @@ "general", "tasks", "elastic-provider", - "jsonl-archiver" + "jsonl-archiver", + "file-flinger", + "s3", + "http-client-provider" ], } diff --git a/README.md b/README.md index 395d30cd..797ec695 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ The complete framework is composed of the following packages: - [**@mdf.js/doorkeeper**](https://www.npmjs.com/package/@mdf.js/doorkeeper): Package for managing the validation of json schemas, based on [**ajv**](https://ajv.js.org). - [**@mdf.js/faker**](https://www.npmjs.com/package/@mdf.js/faker): Package for generating fake data for testing. - [**@mdf.js/firehose**](https://www.npmjs.com/package/@mdf.js/firehose): Package for data ETL pipelines creation and management. Works together with the providers. + - [**@mdf.js/file-flinger**](https://wwww.npmjs.com/package/@mdf.js/file-flinger): Package for managing file processing and uploading. - [**@mdf.js/logger**](https://www.npmjs.com/package/@mdf.js/logger): Package for logging management. - [**@mdf.js/middlewares**](https://www.npmjs.com/package/@mdf.js/middlewares): Package with a set of middlewares for express applications. - [**@mdf.js/openc2-core**](https://www.npmjs.com/package/@mdf.js/openc2-core): Package for managing the OpenC2 protocol, internally used by [**@mdf.js/openc2**](https://www.npmjs.com/package/@mdf.js/openc2). + - [**@mdf.js/tasks**](https://www.npmjs.com/package/@mdf.js/tasks): Package for managing tasks execution: tasks limiter and tasks scheduler. - [**@mdf.js/utils**](https://www.npmjs.com/package/@mdf.js/utils): Package with a set of utilities for development: - coerce: Functions for data type coercion, specially useful for environment variables and configuration files. - retry: Functions for retrying functions. @@ -78,12 +80,14 @@ The complete framework is composed of the following packages: - [**@mdf.js/elastic-provider**](https://www.npmjs.com/package/@mdf.js/elastic-provider): Package for managing Elastic connections, based on [**@elastic/elasticsearch**](https://www.npmjs.com/package/@elastic/elasticsearch). - [**@mdf.js/http-client-provider**](https://www.npmjs.com/package/@mdf.js/http-client-provider): Package for managing HTTP connections, based on [**axios**](https://www.npmjs.com/package/axios). - [**@mdf.js/http-server-provider**](https://www.npmjs.com/package/@mdf.js/http-server-provider): Package for managing HTTP servers, based on [**express**](https://www.npmjs.com/package/express). + - [**@mdf.js/jsonl-archiver**](https://www.npmjs.com/package/@mdf.js/jsonl-archiver): Package for managing JSONL archiving. - [**@mdf.js/kafka-provider**](https://www.npmjs.com/package/@mdf.js/kafka-provider): Package for managing Kafka connections, based on [**kafkajs**](https://www.npmjs.com/package/kafkajs). - [**@mdf.js/mongo-provider**](https://www.npmjs.com/package/@mdf.js/mongo-provider): Package for managing Mongo connections, based on [**mongodb**](https://www.npmjs.com/package/mongodb). - [**@mdf.js/redis-provider**](https://www.npmjs.com/package/@mdf.js/redis-provider): Package for managing Redis connections, based on [**ioredis**](https://www.npmjs.com/package/ioredis). + - [**@mdf.js/s3-provider**](https://www.npmjs.com/package/@mdf.js/s3-provider): Package for managing S3 connections, based on [**aws-sdk/client-s3**](https://www.npmjs.com/package/@aws-sdk/client-s3). + - [**@mdf.js/service-setup**](https://www.npmjs.com/package/@mdf.js/service-setup): Package for managing the setup of the services in edge environments. - [**@mdf.js/socket-client-provider**](https://www.npmjs.com/package/@mdf.js/socket-client-provider): Package for managing **socket.io** connections, based on [**socket.io**](https://www.npmjs.com/package/socket.io) - [**@mdf.js/socket-server-provider**](https://www.npmjs.com/package/@mdf.js/socket-server-provider): Package for managing **socket.io** servers, based on [**socket.io**](https://www.npmjs.com/package/socket.io) - - [**@mdf.js/s3-provider**](https://www.npmjs.com/package/@mdf.js/s3-provider): Package for managing S3 connections, based on [**aws-sdk/client-s3**](https://www.npmjs.com/package/@aws-sdk/client-s3). - Components: - [**@mdf.js/openc2**](https://www.npmjs.com/package/@mdf.js/openc2): Package for managing the OpenC2 protocol. - [**@mdf.js/service-registry**](https://www.npmjs.com/package/@mdf.js/service-registry): Package for managing the observability of the services. diff --git a/docs/assets/hierarchy.js b/docs/assets/hierarchy.js new file mode 100644 index 00000000..bc021825 --- /dev/null +++ b/docs/assets/hierarchy.js @@ -0,0 +1 @@ +window.hierarchyData = "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" \ No newline at end of file diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css index d6dbf458..dffe4b70 100644 --- a/docs/assets/highlight.css +++ b/docs/assets/highlight.css @@ -7,30 +7,34 @@ --dark-hl-2: #CE9178; --light-hl-3: #AF00DB; --dark-hl-3: #C586C0; - --light-hl-4: #0000FF; - --dark-hl-4: #569CD6; - --light-hl-5: #267F99; - --dark-hl-5: #4EC9B0; - --light-hl-6: #001080; - --dark-hl-6: #9CDCFE; + --light-hl-4: #001080; + --dark-hl-4: #9CDCFE; + --light-hl-5: #0000FF; + --dark-hl-5: #569CD6; + --light-hl-6: #0070C1; + --dark-hl-6: #4FC1FF; --light-hl-7: #008000; --dark-hl-7: #6A9955; - --light-hl-8: #0070C1; - --dark-hl-8: #4FC1FF; + --light-hl-8: #000000; + --dark-hl-8: #C8C8C8; --light-hl-9: #098658; --dark-hl-9: #B5CEA8; - --light-hl-10: #000000; - --dark-hl-10: #C8C8C8; - --light-hl-11: #0451A5; - --dark-hl-11: #9CDCFE; - --light-hl-12: #811F3F; - --dark-hl-12: #D16969; - --light-hl-13: #D16969; - --dark-hl-13: #CE9178; - --light-hl-14: #EE0000; - --dark-hl-14: #D7BA7D; - --light-hl-15: #CD3131; - --dark-hl-15: #F44747; + --light-hl-10: #0451A5; + --dark-hl-10: #9CDCFE; + --light-hl-11: #CD3131; + --dark-hl-11: #F44747; + --light-hl-12: #267F99; + --dark-hl-12: #4EC9B0; + --light-hl-13: #800000; + --dark-hl-13: #569CD6; + --light-hl-14: #0000FF; + --dark-hl-14: #CE9178; + --light-hl-15: #EE0000; + --dark-hl-15: #D7BA7D; + --light-hl-16: #811F3F; + --dark-hl-16: #D16969; + --light-hl-17: #D16969; + --dark-hl-17: #CE9178; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @@ -52,6 +56,8 @@ --hl-13: var(--light-hl-13); --hl-14: var(--light-hl-14); --hl-15: var(--light-hl-15); + --hl-16: var(--light-hl-16); + --hl-17: var(--light-hl-17); --code-background: var(--light-code-background); } } @@ -72,6 +78,8 @@ --hl-13: var(--dark-hl-13); --hl-14: var(--dark-hl-14); --hl-15: var(--dark-hl-15); + --hl-16: var(--dark-hl-16); + --hl-17: var(--dark-hl-17); --code-background: var(--dark-code-background); } } @@ -92,6 +100,8 @@ --hl-13: var(--light-hl-13); --hl-14: var(--light-hl-14); --hl-15: var(--light-hl-15); + --hl-16: var(--light-hl-16); + --hl-17: var(--light-hl-17); --code-background: var(--light-code-background); } @@ -112,6 +122,8 @@ --hl-13: var(--dark-hl-13); --hl-14: var(--dark-hl-14); --hl-15: var(--dark-hl-15); + --hl-16: var(--dark-hl-16); + --hl-17: var(--dark-hl-17); --code-background: var(--dark-code-background); } @@ -131,4 +143,6 @@ .hl-13 { color: var(--hl-13); } .hl-14 { color: var(--hl-14); } .hl-15 { color: var(--hl-15); } +.hl-16 { color: var(--hl-16); } +.hl-17 { color: var(--hl-17); } pre, code { background: var(--code-background); } diff --git a/docs/assets/icons.js b/docs/assets/icons.js index e88e8ca7..58882d76 100644 --- a/docs/assets/icons.js +++ b/docs/assets/icons.js @@ -3,7 +3,7 @@ function addIcons() { if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); - svg.innerHTML = `""`; + svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; svg.style.display = "none"; if (location.protocol === "file:") updateUseElements(); } diff --git a/docs/assets/icons.svg b/docs/assets/icons.svg index e371b8b5..50ad5799 100644 --- a/docs/assets/icons.svg +++ b/docs/assets/icons.svg @@ -1 +1 @@ - \ No newline at end of file +MMNEPVFCICPMFPCPTTAAATR \ No newline at end of file diff --git a/docs/assets/main.js b/docs/assets/main.js index 21a5d74d..4f59cd95 100644 --- a/docs/assets/main.js +++ b/docs/assets/main.js @@ -1,9 +1,9 @@ "use strict"; -window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings."}; -"use strict";(()=>{var Pe=Object.create;var ie=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var _e=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Me=Object.prototype.hasOwnProperty;var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var De=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _e(e))!Me.call(t,i)&&i!==n&&ie(t,i,{get:()=>e[i],enumerable:!(r=Oe(e,i))||r.enumerable});return t};var Ae=(t,e,n)=>(n=t!=null?Pe(Re(t)):{},De(e||!t||!t.__esModule?ie(n,"default",{value:t,enumerable:!0}):n,t));var ue=Fe((ae,le)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,u],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. -`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[u+1]*i[d+1],u+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}s.str.length==1&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),m=s.str.charAt(1),p;m in s.node.edges?p=s.node.edges[m]:(p=new t.TokenSet,s.node.edges[m]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof ae=="object"?le.exports=n():e.lunr=n()}(this,function(){return t})})()});var se=[];function G(t,e){se.push({selector:e,constructor:t})}var U=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){se.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!Ve(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function Ve(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var oe=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var pe=Ae(ue());async function ce(t,e){if(!window.searchData)return;let n=await fetch(window.searchData),r=new Blob([await n.arrayBuffer()]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();t.data=i,t.index=pe.Index.load(i.index),e.classList.remove("loading"),e.classList.add("ready")}function fe(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:t.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{ce(e,t)}),ce(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");i.addEventListener("mouseup",()=>{te(t)}),r.addEventListener("focus",()=>t.classList.add("has-focus")),He(t,i,r,e)}function He(t,e,n,r){n.addEventListener("input",oe(()=>{Ne(t,e,n,r)},200)),n.addEventListener("keydown",i=>{i.key=="Enter"?Be(e,t):i.key=="ArrowUp"?(de(e,n,-1),i.preventDefault()):i.key==="ArrowDown"&&(de(e,n,1),i.preventDefault())}),document.body.addEventListener("keypress",i=>{i.altKey||i.ctrlKey||i.metaKey||!n.matches(":focus")&&i.key==="/"&&(i.preventDefault(),n.focus())}),document.body.addEventListener("keyup",i=>{t.classList.contains("has-focus")&&(i.key==="Escape"||!e.matches(":focus-within")&&!n.matches(":focus"))&&(n.blur(),te(t))})}function te(t){t.classList.remove("has-focus")}function Ne(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=he(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` - ${he(l.parent,i)}.${d}`);let m=document.createElement("li");m.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=u+d,m.append(p),p.addEventListener("focus",()=>{e.querySelector(".current")?.classList.remove("current"),m.classList.add("current")}),e.appendChild(m)}}function de(t,e,n){let r=t.querySelector(".current");if(!r)r=t.querySelector(n==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let i=r;if(n===1)do i=i.nextElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);else do i=i.previousElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);i?(r.classList.remove("current"),i.classList.add("current")):n===-1&&(r.classList.remove("current"),e.focus())}}function Be(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),te(e)}}function he(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ee(t.substring(s,o)),`${ee(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ee(t.substring(s))),i.join("")}var je={"&":"&","<":"<",">":">","'":"'",'"':"""};function ee(t){return t.replace(/[&<>"'"]/g,e=>je[e])}var I=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",ye="mousemove",N="mouseup",J={x:0,y:0},me=!1,ne=!1,qe=!1,D=!1,ve=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(ve?"is-mobile":"not-mobile");ve&&"ontouchstart"in document.documentElement&&(qe=!0,F="touchstart",ye="touchmove",N="touchend");document.addEventListener(F,t=>{ne=!0,D=!1;let e=F=="touchstart"?t.targetTouches[0]:t;J.y=e.pageY||0,J.x=e.pageX||0});document.addEventListener(ye,t=>{if(ne&&!D){let e=F=="touchstart"?t.targetTouches[0]:t,n=J.x-(e.pageX||0),r=J.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(N,()=>{ne=!1});document.addEventListener("click",t=>{me&&(t.preventDefault(),t.stopImmediatePropagation(),me=!1)});var X=class extends I{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(N,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(N,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var re;try{re=localStorage}catch{re={getItem(){return null},setItem(){}}}var Q=re;var ge=document.head.appendChild(document.createElement("style"));ge.dataset.for="filters";var Y=class extends I{constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),ge.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=Q.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){Q.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var Z=class extends I{constructor(e){super(e),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.dataset.key??this.summary.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`;let n=Q.getItem(this.key);this.el.open=n?n==="true":this.el.open,this.el.addEventListener("toggle",()=>this.update());let r=this.summary.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)}),this.update()}update(){this.icon.style.transform=`rotate(${this.el.open?0:-90}deg)`,Q.setItem(this.key,this.el.open.toString())}};function Ee(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,xe(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),xe(t.value)})}function xe(t){document.documentElement.dataset.theme=t}var K;function we(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Le),Le())}async function Le(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let n=await(await fetch(window.navigationData)).arrayBuffer(),r=new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();K=t.dataset.base,K.endsWith("/")||(K+="/"),t.innerHTML="";for(let s of i)Se(s,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Se(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',be(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let u of t.children)Se(u,l,i)}else be(t,r,t.class)}function be(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=K+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else e.appendChild(document.createElement("span")).textContent=t.text}G(X,"a[data-toggle]");G(Z,".tsd-accordion");G(Y,".tsd-filter-item input[type=checkbox]");var Te=document.getElementById("tsd-theme");Te&&Ee(Te);var $e=new U;Object.defineProperty(window,"app",{value:$e});fe();we();})(); +window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings.","hierarchy_expand":"Expand","hierarchy_collapse":"Collapse"}; +"use strict";(()=>{var De=Object.create;var le=Object.defineProperty;var Fe=Object.getOwnPropertyDescriptor;var Ne=Object.getOwnPropertyNames;var Ve=Object.getPrototypeOf,Be=Object.prototype.hasOwnProperty;var qe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var je=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ne(e))!Be.call(t,i)&&i!==n&&le(t,i,{get:()=>e[i],enumerable:!(r=Fe(e,i))||r.enumerable});return t};var $e=(t,e,n)=>(n=t!=null?De(Ve(t)):{},je(e||!t||!t.__esModule?le(n,"default",{value:t,enumerable:!0}):n,t));var pe=qe((de,he)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,c],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[c+1]*i[d+1],c+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}s.str.length==1&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),m=s.str.charAt(1),p;m in s.node.edges?p=s.node.edges[m]:(p=new t.TokenSet,s.node.edges[m]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof de=="object"?he.exports=n():e.lunr=n()}(this,function(){return t})})()});window.translations||={copy:"Copy",copied:"Copied!",normally_hidden:"This member is normally hidden due to your filter settings.",hierarchy_expand:"Expand",hierarchy_collapse:"Collapse"};var ce=[];function G(t,e){ce.push({selector:e,constructor:t})}var J=class{alwaysVisibleMember=null;constructor(){this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){ce.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!ze(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function ze(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var ue=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var ge=$e(pe(),1);async function H(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0)),r=new Blob([e]).stream().pipeThrough(new DecompressionStream("deflate")),i=await new Response(r).text();return JSON.parse(i)}async function fe(t,e){if(!window.searchData)return;let n=await H(window.searchData);t.data=n,t.index=ge.Index.load(n.index),e.classList.remove("loading"),e.classList.add("ready")}function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:document.documentElement.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{fe(e,t)}),fe(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");i.addEventListener("mouseup",()=>{re(t)}),r.addEventListener("focus",()=>t.classList.add("has-focus")),We(t,i,r,e)}function We(t,e,n,r){n.addEventListener("input",ue(()=>{Ue(t,e,n,r)},200)),n.addEventListener("keydown",i=>{i.key=="Enter"?Je(e,t):i.key=="ArrowUp"?(me(e,n,-1),i.preventDefault()):i.key==="ArrowDown"&&(me(e,n,1),i.preventDefault())}),document.body.addEventListener("keypress",i=>{i.altKey||i.ctrlKey||i.metaKey||!n.matches(":focus")&&i.key==="/"&&(i.preventDefault(),n.focus())}),document.body.addEventListener("keyup",i=>{t.classList.contains("has-focus")&&(i.key==="Escape"||!e.matches(":focus-within")&&!n.matches(":focus"))&&(n.blur(),re(t))})}function re(t){t.classList.remove("has-focus")}function Ue(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=ye(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` + ${ye(l.parent,i)}.${d}`);let m=document.createElement("li");m.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=c+d,m.append(p),p.addEventListener("focus",()=>{e.querySelector(".current")?.classList.remove("current"),m.classList.add("current")}),e.appendChild(m)}}function me(t,e,n){let r=t.querySelector(".current");if(!r)r=t.querySelector(n==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let i=r;if(n===1)do i=i.nextElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);else do i=i.previousElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);i?(r.classList.remove("current"),i.classList.add("current")):n===-1&&(r.classList.remove("current"),e.focus())}}function Je(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),re(e)}}function ye(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ne(t.substring(s,o)),`${ne(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ne(t.substring(s))),i.join("")}var Ge={"&":"&","<":"<",">":">","'":"'",'"':"""};function ne(t){return t.replace(/[&<>"'"]/g,e=>Ge[e])}var I=class{el;app;constructor(e){this.el=e.el,this.app=e.app}};var A="mousedown",Ee="mousemove",B="mouseup",X={x:0,y:0},xe=!1,ie=!1,Xe=!1,D=!1,Le=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(Le?"is-mobile":"not-mobile");Le&&"ontouchstart"in document.documentElement&&(Xe=!0,A="touchstart",Ee="touchmove",B="touchend");document.addEventListener(A,t=>{ie=!0,D=!1;let e=A=="touchstart"?t.targetTouches[0]:t;X.y=e.pageY||0,X.x=e.pageX||0});document.addEventListener(Ee,t=>{if(ie&&!D){let e=A=="touchstart"?t.targetTouches[0]:t,n=X.x-(e.pageX||0),r=X.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(B,()=>{ie=!1});document.addEventListener("click",t=>{xe&&(t.preventDefault(),t.stopImmediatePropagation(),xe=!1)});var Y=class extends I{active;className;constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(B,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(A,n=>this.onDocumentPointerDown(n)),document.addEventListener(B,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var se;try{se=localStorage}catch{se={getItem(){return null},setItem(){}}}var C=se;var be=document.head.appendChild(document.createElement("style"));be.dataset.for="filters";var Z=class extends I{key;value;constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),be.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } +`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=C.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){C.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var oe=new Map,ae=class{open;accordions=[];key;constructor(e,n){this.key=e,this.open=n}add(e){this.accordions.push(e),e.open=this.open,e.addEventListener("toggle",()=>{this.toggle(e.open)})}toggle(e){for(let n of this.accordions)n.open=e;C.setItem(this.key,e.toString())}},K=class extends I{constructor(e){super(e);let n=this.el.querySelector("summary"),r=n.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)});let i=`tsd-accordion-${n.dataset.key??n.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`,s;if(oe.has(i))s=oe.get(i);else{let o=C.getItem(i),a=o?o==="true":this.el.open;s=new ae(i,a),oe.set(i,s)}s.add(this.el)}};function Se(t){let e=C.getItem("tsd-theme")||"os";t.value=e,we(e),t.addEventListener("change",()=>{C.setItem("tsd-theme",t.value),we(t.value)})}function we(t){document.documentElement.dataset.theme=t}var ee;function Ce(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Te),Te())}async function Te(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let e=await H(window.navigationData);ee=document.documentElement.dataset.base,ee.endsWith("/")||(ee+="/"),t.innerHTML="";for(let n of e)Ie(n,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Ie(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',ke(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let c of t.children)Ie(c,l,i)}else ke(t,r,t.class)}function ke(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=ee+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else{let r=e.appendChild(document.createElement("span"));r.innerHTML='',r.appendChild(document.createElement("span")).textContent=t.text}}var te=document.documentElement.dataset.base;te.endsWith("/")||(te+="/");function Pe(){document.querySelector(".tsd-full-hierarchy")?Ye():document.querySelector(".tsd-hierarchy")&&Ze()}function Ye(){document.addEventListener("click",r=>{let i=r.target;for(;i.parentElement&&i.parentElement.tagName!="LI";)i=i.parentElement;i.dataset.dropdown&&(i.dataset.dropdown=String(i.dataset.dropdown!=="true"))});let t=new Map,e=new Set;for(let r of document.querySelectorAll(".tsd-full-hierarchy [data-refl]")){let i=r.querySelector("ul");t.has(r.dataset.refl)?e.add(r.dataset.refl):i&&t.set(r.dataset.refl,i)}for(let r of e)n(r);function n(r){let i=t.get(r).cloneNode(!0);i.querySelectorAll("[id]").forEach(s=>{s.removeAttribute("id")}),i.querySelectorAll("[data-dropdown]").forEach(s=>{s.dataset.dropdown="false"});for(let s of document.querySelectorAll(`[data-refl="${r}"]`)){let o=tt(),a=s.querySelector("ul");s.insertBefore(o,a),o.dataset.dropdown=String(!!a),a||s.appendChild(i.cloneNode(!0))}}}function Ze(){let t=document.getElementById("tsd-hierarchy-script");t&&(t.addEventListener("load",Qe),Qe())}async function Qe(){let t=document.querySelector(".tsd-panel.tsd-hierarchy:has(h4 a)");if(!t||!window.hierarchyData)return;let e=+t.dataset.refl,n=await H(window.hierarchyData),r=t.querySelector("ul"),i=document.createElement("ul");if(i.classList.add("tsd-hierarchy"),Ke(i,n,e),r.querySelectorAll("li").length==i.querySelectorAll("li").length)return;let s=document.createElement("span");s.classList.add("tsd-hierarchy-toggle"),s.textContent=window.translations.hierarchy_expand,t.querySelector("h4 a")?.insertAdjacentElement("afterend",s),s.insertAdjacentText("beforebegin",", "),s.addEventListener("click",()=>{s.textContent===window.translations.hierarchy_expand?(r.insertAdjacentElement("afterend",i),r.remove(),s.textContent=window.translations.hierarchy_collapse):(i.insertAdjacentElement("afterend",r),i.remove(),s.textContent=window.translations.hierarchy_expand)})}function Ke(t,e,n){let r=e.roots.filter(i=>et(e,i,n));for(let i of r)t.appendChild(Oe(e,i,n))}function Oe(t,e,n,r=new Set){if(r.has(e))return;r.add(e);let i=t.reflections[e],s=document.createElement("li");if(s.classList.add("tsd-hierarchy-item"),e===n){let o=s.appendChild(document.createElement("span"));o.textContent=i.name,o.classList.add("tsd-hierarchy-target")}else{for(let a of i.uniqueNameParents||[]){let l=t.reflections[a],c=s.appendChild(document.createElement("a"));c.textContent=l.name,c.href=te+l.url,c.className=l.class+" tsd-signature-type",s.append(document.createTextNode("."))}let o=s.appendChild(document.createElement("a"));o.textContent=t.reflections[e].name,o.href=te+i.url,o.className=i.class+" tsd-signature-type"}if(i.children){let o=s.appendChild(document.createElement("ul"));o.classList.add("tsd-hierarchy");for(let a of i.children){let l=Oe(t,a,n,r);l&&o.appendChild(l)}}return r.delete(e),s}function et(t,e,n){if(e===n)return!0;let r=new Set,i=[t.reflections[e]];for(;i.length;){let s=i.pop();if(!r.has(s)){r.add(s);for(let o of s.children||[]){if(o===n)return!0;i.push(t.reflections[o])}}}return!1}function tt(){let t=document.createElementNS("http://www.w3.org/2000/svg","svg");return t.setAttribute("width","20"),t.setAttribute("height","20"),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.innerHTML='',t}G(Y,"a[data-toggle]");G(K,".tsd-accordion");G(Z,".tsd-filter-item input[type=checkbox]");var _e=document.getElementById("tsd-theme");_e&&Se(_e);var nt=new J;Object.defineProperty(window,"app",{value:nt});ve();Ce();Pe();})(); /*! Bundled license information: lunr/lunr.js: diff --git a/docs/assets/media/Provider-Class-Hierarchy.png b/docs/assets/media/Provider-Class-Hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..307e24cda2c1efbe3ee60c2ba9c81cd54183ebcd GIT binary patch literal 47022 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 C)6l~J literal 0 HcmV?d00001 diff --git a/docs/assets/media/Provider-States-Events.png b/docs/assets/media/Provider-States-Events.png new file mode 100644 index 0000000000000000000000000000000000000000..b9d8a38044a50a1802c8f9ea2bc8fe54c700bd5a GIT binary patch literal 23046 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

s7AiKHbl$ANCBi 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 literal 0 HcmV?d00001 diff --git a/docs/assets/media/Provider-States-Methods.png b/docs/assets/media/Provider-States-Methods.png new file mode 100644 index 0000000000000000000000000000000000000000..5ca00ec2244ac40b3c7f291e8ebb145298c80330 GIT binary patch literal 34090 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 c9$XQ8HBY1pZ+?aH&q8EgD@qi + +

@mdf.js/firehose Module

Writable Stream

Writable Stream

Transform Stream
(Strategy)

Readable Stream

Readable Stream

Readable Stream

Data Flow

Data Flow

Data Flow

Pipe

Pipe

Pipe

Pipe

Pipe

Processed Data

Processed Data

Source.Plug.
Flow

Source.Plug.
Sequence

Source.Plug.
CreditFlow

Data Processing &
Transformation

Sink.Plug.
Tap

Sink.Plug.
Jet

Database or
Data Storage

Message Broker

Data Warehouse

Analytics Platform

\ No newline at end of file diff --git a/docs/assets/media/logging-capture.png b/docs/assets/media/logging-capture.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccdc9551f512bd21cf54582a915680dfd308f2c GIT binary patch literal 137218 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

uT5=Ea&#^+L1Qga4@_obqJ|qX*d1| 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

I%G;^pvIom8;B?g{mN=}ALt!ax z8Czk4kbeCl27I?~(UObuy_lGzn7@Wz#`it*QXMCpcph%2(2tbGH*U*JuM|MhDKgmvi z0i$uCmQv^f!795xJZ51iDoJD~kmvnMlc=G^8??uk0L7JV6k5ffe^ta{f?4bk{6UlA zTm6v%s;c3c3eB^(xU6_Bd9)YJK=IDbr-CbB!b;WD79M6j$CURw)8G?pM7>Wfv-X^m zF4pP?p5EVN1~X5ERr<%mRR`B;+dFN?@j7e;&AQ_=YPy#4iB{eOWUIb$a*{xwnC*!v z?COT5n8u60=s>UjYHLd{97RLeT;hHdytVl?3!;D9b>mpQN6jPE#P*T2{X;rQQQK5M ziw^zg_nft2(rfq6MM4t29Gm)AoSs4@^lAl`zuyE5`|APW`W71*8P_##6{GJF$jl*qfzb`9d<@mA76F%r7aTS?t@Q8TRkT~52n7S zzWAwKC&_J}w3B$%BS3Z`J=m|5FVa$h4A*ieT1ZTqR z)L&sEO`+(n4sfT|F(VssBq;f%$P|VXfuV1{p(mb?z*LN*|SIBGz!HCT>;X>6fRh-3huPCmqy=*aJjAMts z092s4UC-V)J|0nEV8ihNeQCZVwl-6&L#e#$Tkxbi<9$#(t@gWDUkp`@f2QHOsyWLd z9gce~Lq`=K7x{-jPkUZv9WvOMB``#1E|K}F<-yL~Yv=9xd|@KjL7zSDni4g1ouI8z z?C=-eU4`luwlvG5idQ4ZVl6D~I$VNYpfp`J)#K16ykU-q_W&(t-nqH+s4&b67l6XT zU|ZBh&QZ+Rj-C??@Pgs2EL39-?<;D&T02L>hKHA)kzg` z(SNg-ymU&srfu1F#Z*j72Mp4q?unZW@e5tx5puUQTzNj+O^4B#9asBPn|KCfvpdq;fiHw%=#_yU2xLtKsU3(c4uSMVNgjCSvkXRmwTmH9-0 z1J>GSA6arLoSe8A`%@q8ho5iwaRIQc8m{?m9?YCPCi$nKF%^x||OJdo>n79(`rj}nEH$#ii@Td2U~ zor6av4IjtD@R3y>AE$*Jn}uF3KaQh3#z`@>B|MI9{vg%1K`1oU&ub5@#r$ zO&GBdwcKC!`m<%RdSk+bg0A$M{o6{bek%dB7in6%tr4bW>#1CQ8dp{G`I^BOAD`Cm zQxWSdjCq*OF@`ZJ5-gVl>aE2p7N{f)K4dXEu~sAu&||ucS$bWl>+rEyp(ZC3M~TLF^ejs_<~>pQ=}Vi4KGjO-}E z&8a`aVbCvO($>eB*Cuc7tll8Ej?KliB(EAXPkxKJ)v;etmQhT*ykHvZpnd9PN*jUK zH|Vul%&5QF{o6}E@N?-RgFP-4f1}Jf7d+b0YiX#`uk;EC#ob2QhNzzR0;;C*$^8D1fO>v*$1XB6Ve^lqqPu zZj~~&iP7Q!Xx#fM7al~@FkvC>>2=iU{g}&N4{5E#y)QN%5=LGd&?e3|$w%Crc^Y>C zrocv^HzdtywCVD6R!?_Ij-9gYEciJ6($z_l{i;R6Evc7+{9z|Jw_cSp?qx+Urj~^| ze6bf9mz*6|*}K{rG#!!qYJL>k^NnD%R+yyyMEzOg(#2=Y=t+(4bBocQOktjwElJTSp~FSeBN>YmTTzFy}%3ABtyI?+Eg z_o@msy4}(k?oY0%$MFNUaH~yr;T=!mVbK1KQGR0aTYjjA*>2d#cA$x^aeZJJVMT>y zjUrCigHsfniHz;A(KGN1J4Vxb&vlB@@#ewbprg|+10&8_cdw!a@v^&N88kgz(TO_k z@7Cy5-qfHmlG#?#HIQ{^}7+6!+L971XlZbISjvw_8WV z5zcn{2Tzv+LxzaUd4BYu#ezI3*q~O(qZ-YU5ySpnU&`mG3R_a&6m)yz{;CmRC%W#4 zET#+Rw6yX|7W@ztY4|%3v=o?=&MAkI!B|5Xp1Y^IK6y#>YSp^Vhg#BAXsgwlb&)rS zTBVme>#;`4ryRa(YJyx?oJVg-ecjfgOKA5%n0*hu;QnI`lD?%B{^zy`C@+`ogYI&X z_y7_u%MY82ODT~0NTuq*nk6qGx-DX{91M23zI&WBZh?6?vFBCMCy?VnIRRD^et0D4{?XyLpnrP{zb|<;lp}9x zp|h&Lt`u`7sEZ3fk2CoFgyd+j!l^+lCWU6T=wi%N`xIpAWu?Ddqjl|;(Qk*h1CUwu z#RcMdGG-3{Z0@U=uaOv+5Bi=w=_5eHavMWmSSuG!4iO#$tLCLzQ)K$TSz4(ZAF^=x$eb;oZb`=3Up34c;s zc7b2&-0RtSdEM&6s3HD^~C7(G%)7*S^?=5>1@qxCilUBEn&4Pu^vW6{v2`L`) ze`^C5*JW@^1cyVHN*53Q(zZeRItMc0ORfCf@tKxB^QyxihDGdr<}Ag0VFO1L_@RMp zxF?p~_&l)3RL1+V2bFvUG`74M|NP1rh6W-lU;a+(4%B$f+v6PN)HKN7ssn$k&!N;J zZ{|e(6=*AbRnaeM_9Fw~dH?uzjHq$%GY99Rs|8H&aZ)o`(AxUq!FTgXGNtVo?dBm! zc#7#o!YXShl(prylB}JzHnmwGza4x~zL&O!N;HQB54P5~ajLg3CKb|d({mJ0rFu99 zr+VHS6VblpFjObgG^1xoxvUX*(f+D2b8C^IW{+*Wr)KKigYQEuDdf?w$j+!++w=5> zb3DJ~WV3Hx56~!lelKV%AKi5>$PaaC&%o|ocBEFU0{`;Z?FF&WQMX?9wlBs4Z(H@% z(^!~#|M;2jV@C2S;zL5E%4?PllaWtYFw13;h93SYa{qXtJNoLze(T=*({muQtO@zx z9Y6%_d$6vxD3wEY`o!Ud2u8|1am~LK02J~6(Pzlkv+-1JKr5oXwU28^z%DcjH0ne| zZ4Nwsapngmdv+`P-KhV{{eNQp7dgH4arqKU7EMmyk40-Cv~m7E&YW3j7xiLJL5Kfj z6uG>s*G&393L|6KD--!M7`KqX!MUIXRv}0qR-#ILws`w;`+%GNkBu9B zL)Syl@A@}9b+5am&5BAGvLi~)zw}QvfSC*e!}e<@YS7BMH2bNRz^Q`6N*0|*y5Bk( zAOK{2S0=u!0g#(NgQLTI`!;#Y6i%#VZydKhQ;!(pv(ahq$p_j&V$?}jsZV_W4p6E4 z5lz z)~lI)4O3+J{ZY|AADc(SJuCqQiC$ZY`aldXFE&YHeLl^>q(W9jbr9=9v@`J^u2{XSoQc6bjnjaG_q3>3sT@Ks zo&2MRCOyhkWI&yj%4A9&8RTomHJr~{?%Vg^aPBE`35;88^MFnY@3f8#MC;%92-Mb? z<`5?Pz9^dC^3(;-39F6P6z7@qzq$0Dd`(3^!juXwsq7Pi{ZZmb$EOJR?f-5^fBYF} zPLC-77B7vP{jR%Cf3!qxA*cmMcb74cqV^Swf$T$zew_1Xdj|-{E;qZOME%h*FZx1T zvy}m{_ko_n@$MUf`vnS~?Qd&2TOtM6CFNtpZk{_$lJ2s**?GHA4eihr+)bbGQX%yF z-%DSoV4p_ex-W!_UfUrO2ezj7i@Y!M6H6tt+LP#hA2)19w9pp<2AuOH+kYyG%5v>% zB3mM?JHbrv*J}267ZD?{le45u9yDvLU!G5?C%|N0z{Bf)v8a@$7MLh+lc9D%VC2*L zEo$DF43WmD03?IFu%-({4hR;tef46xv3bUED@X2>uN~-1eRFdoK(qlL%^F3i%_XkR z+lgvNU5||J-h}e)MXOP{t*x!?#Lo9_rk|bI{ldM|P%X?D01j!bG*|wV*A*Yk32H&L zkA?o3R@(GsE94A)g{XVWbCfsNfY|o(OqS?I4=X(fc~pX%DrRJX_yXKidB2}rqJ%XtmxSn@^Y=1JgyRbILDw<=JD&miwh1jxDVXi_om)mD$cq4X8D~dck%&Fufh1) zB`0$;>gYq}DNaPA?$>mYGCs*EL0Mh&gwz zc4lO=3`F9gU+eo;`)qHr#W`%PTckxb-1hiHe2>#k8|^W7wgCErdBB5faoUem9kN;B}Zi*r%E{rz*D(azBea=|el zX9;PqgYl!7y^LWWW5wEu5NGi-^Y(-VnqOplJP(HtbA;>c_acD4VKvl$?8!L?s2e`7 zyNfMRN{}q8A#Gmfa>d_xc;3~T`DO+8POa!-o!tm+PrjiExSI3Fm0p}2(d68?i@=qn z(cx;DGFK(P**eNkC)=gF+~tpQAi&X$HbV@rG(+{As2ywrjGeQAlD~|viu98Zsb(Xc z%cJu_n5Ku@0?p3qoy3vF1D!|D#Zf<8b~o=EO|%3^FpKS`-^cyL$QgKI_ zE1oPUoEADz$j&%zsUNQ;v7)DoS{Lu4sw*K9HbJ%hRLQ3SB{>AE0ZF*gP7PyYyk8I= zQ*LlWXfMa_^f(A>l4C$V&?{j)f-b_{&?W;^0WM}r5}7q5McyaSlsifN2LvByu0=0# zDZW>>XeCje1)l2iN}iV$61K^5TEqum?Xe6QW{SMA2s6L>wm24am(-LluZ~{+vLcX| ztk&WIyHve>AhN4=ag_a4S(Bt#;W#XU=iAx5OlifB(Wg%gQwji2N%NEwNPhBX;|~1- z-f~R41h`~Y8msf37mCD=wR7y}AONd+V`K@un@jsTdI-b7wjx`?6gdEp*XqmXp`-$X zyN4IV4v*>pc7++gE(J~k7QbyV1)`R`0T=-*U#Nkk{Tb0m5`pk?sY41=mnhnk-?u1e z1WmeUaZgkH1<;m(B12i3)K=NYxY)VusJOSY`g~*)VM|{a)FN3VF`nM%D%O&*8DOhX z$=<+7KBzvGdG@^0+)4Gy6J~B}t;g%^1L7-p*c1}yyN;x8T0Aj0sVLFb&g$N?+S+^s zIb+M_9?aXh-aI8YD}=_lkC7PL%7J;;+>!*>z4w0559qke%A5+B$kGxKjR(yRoJO4! zBhtmw>8AFuac?sgg#EZxm3z|d&qy*cnW6?Y3MKPWy2Rbtk)>HMP;b-G^ARyMNRtMh_mJP3D_W%tGO> z|8_N(8w-y`g_<|qA47JPp?B|&=YsRr_fEW+^hj?tScT>fcpF-Y_eeRZV&F2LXp_oME>CA4QoAux5 zHhh!P_(_G=*@KU;;Z+i{0OO{4=x%aNZyo*?GCu~s2&SHIazlfNPD+cQ>{TwM7MFh$ zTrpcubWC4-;0f7Ouq(bMiGzOw#9cYz9;v&g%(Tdf>nZdGgf;8+N_6SPFw!bEUyQja z48FwXG!yU~b0bbhj|2@GS3zr$89*8_T-^Ty^0s|>F_=eWrq}jp!U>7)yZm3W+)k9x z?wNNJpY#m~Bb@V8cb?vFiwWTsf1$eiBi{jabf+}|FE_UPo?Oz*-vUd3@C{SI=5~t-b$LCfoR7x%ClT+y+)bw&t7bkL^ZF6C z17Pr|EAS}lFQLO6m#4K&t&~ov`|v)1;-n$DNh8U!kP-LyZAD0R9XO@QjSzF)`e2msfjkx3aKh!0O~?2pLHxRr zu}-Em%IXXwNNqt1pqPfuhAXBu(6ck;pW(FYhZ`xJBtltN4<7I0Ps3M-MHv0O@=c0DmafP>c zJPr{^ZWG>W9+ZH;n^b$l9>rD4ZBOXz)R5c_T*Zb&2$ixpEDb<|da@c~nTZLdbrL2i z)vZ1i%|c%3?7;&mW#;~-d3Esn9z%P(_!WbmP*;hJz=K3(2|WJV1U8=cz_w+D=oZPSf~*QeATrCU<$8Q?X2GtCAv%RN0t z4Uc(Y6JUw*L}ju>TERTyeZTCVXY<%bJM%}`>CNe(W?I)q(uequDe5e_C7~6N$?4dW z5Cd%{>#Aj*@w)i8RQ9$XsvNx&xycyPCbJP)hKz1(RplzCj74a}@$Q}qIfi}CHl=jV z_{svLJCZfJ`rk;jA;+2r#X*!9``p9i+pAUB9lE}CL4ZlfQ^ROz_o_?HkvbZkh>p)2 zE6C^l8TTo2|6i(l9X?+F!tg_%pvt28qBI$%W46x#cLQGp^9;n+svelJNsw zUvC9DGx{nuJ_<}7wWg3*dW19^1<#9*ebAxgjPHvpvC^M62LLeNi)Qskd&!-& zby8}VVbI|ipLwk$;Bei$x?;(kaYBTLzW2~8Du1$J96wBq{3JV^&gBwgT)6u?2jY4$ zc1yRHJ^Myh0FkNlX^}O0F*WQn^04}CxY6l%r z@II*NRyWwygoj1BH_jNyEli}YVML$pNF)%z9q}pAP_2E$u#`*_aZrxnQ?hx9g*0?g z(c~*6eG0XGxfWRx!?(YEJdEI9$!PoTQEs(1?1v6Khsoz{0P9SDmcl}}4@87c)dbBP zukn-6nWCJo0TwD1X_z?=EF=T0W4k1J#xvu%k+y#4CzJE^dXT`va_dZMB^OTFsBrM0 z`Y)t>z|!W#9OOWuChZxajUMl5T~{)Xr>~SVI9QRmce}y`-M<~OZ?J8OhKv@h;Nd$o zBBTy{``F|Ympe^@a^s!)XDRii0efu0-0R~z?!YJYq)7<%C&nN6T~e|==d2)flpl0n zl|gx;=a>7O=x#H3lyV@oJfb7S%UKbQbM@vJA8vKLvY;B|qPF?QE$S{mE@s3WPZ z)UC2PnZx;N0Z5Lps?P(_K*A#SU(s!v;Nj%Q2{E?c?J(_{shhPaX;B2wqZ>2f^Z9t^ zCW6tOh{3Ff4(a?1CtB~A#wEv-cQcqEa`QV>-NL`*_u_m;Gnr}_GAaS?=+s3?Seh17 zmtxm-g40(nVBw;;A}@a)O^~3|<^Rnr`MtBzVIM6A>Y}@ZNrg|hADTU`vlhLW(gwFhVr{Ab z{<73gAAcrm>Cx+gduuJ4mUh>JP4`fd>=lM$|87*-bD!`O_9dMjUE;(VT2im zmYBVXUIO zg1TPxxC_e~=84CuWkApznRvM?#_^1vVWzgzIawWw5ZnhCKSxT3KW?i@ERJ{A0SS6O*d15f9>78r z#zVyV_#HbU6=GEFuJaRdpJK;HQ!0Nb@Dvz3fyJH(y!VRkl-0rdmZR230P%m4);UEd@uAr z@iYlsn>c3u&ZzNZcH9eTul6Lv@!2Z=|3+-i*y`_iCI_^KA&7=X*G~-(dk#Yn9HJGj zNLZABoNhHL!04O>zlFMyADxW#F+M?=_m3debH^Zj)zU6BJHJ|505+f+=8`v8eVhFQN;WeymmXoM_AR2cQx z37?X&&UMlt)Cb)J*GM{4w0>L~RPgJhP5rHnlKJ*BAAWDNOVc-xt9ei z>e?PRkDbpYsH`~i<)*c*?cQ4XdvVhW{-g4Q+ujOL;C{GF2%abY8xK)v?m;m?b!tz= zqizYK=Y_)?RpwqUeRSYawEd5Pnji3ddIu!&X$7wYy(QfDh&tY-Sa%*Avfu>0ciiU$ z)4ZZw;wBzO5BX>lXG8htwJwsi?oj%0H=Wt-(1a(HAqp<{2J@v{oYpAwU&HWN!q6^D zz${aTxX*t;t$Q-lZSn21I(I}dACpyDe0~m-9h4?B{~(AV@2iV){3jpcW^MR(rI8P+ zFO)2~`p)=m1sR*hIX)3z+mOFjsW^MUXc+=lSg(!bRYTR4VgjicDTSMEzMz@bItJ~a z>}-k0H399785`POq7GH;#fwXncfx>hV{_ddUopGL!QZ~_l_&gSOhs9+mmVpJc&LRe zWBT9mKgE{L{dmb@)`sf&(f^M-sCyM~a7*$8;ft96i@EmzYVzIJb_EnrAc#m0prTZz zmq6$!MUf(1x^$2ddhbo?(z}Q>fq?WL5b3>0htPZPgmzvm|5f%{`|Nf0cV@nsj02K{ zOn8$#@9%o<>z*v{#G-{RtsSNoFgd#Uw3S`XI(;QCoilzp7BVP7R2vFx04(TqI&VQB z)Yx&Bn1WFGQ1Ph!u!IT@_FQgpZyjXog!WK#&0)uh^QIh&9d!vJrxqD&LVGSMx=rU_f8AO;EQycac$*4} z&?c$U%q+S!DADB@L#bdo{ye*Lj-gnNqN9VS&?NTgwCGqYy98t7F(fl;P$I6mLrH_$ zJ+Ym^C@{e~x&*`T{N+>4>vIlUOIy^W89}*lldDO+?)!*2KrbqSwxAK!km-Fx#(?Gv zYiR>|UUNg=>(9wb68f~*-=fg=oYubGo>1K!3qMupH?$gI-NvMHa z+U%Ux#QFJ>`_=Hzax^=#wouHybxboIE$iC6y=Hst(z78FXcnTHd_815EqH zX`t%yJdbC~y!v(s) zO8p^eZz^RnWiEK$sNV#G|9EGEN!C_k3M@XsX?*rSF8jYkM|BulPyh~E1%0j7&OI7v zjlJ<$AnFtlwAMd-OoHlZ_FHUx*hw>_&EwD?sqoa}&efBvYaPBdE%*KzOBW}#8J+-q zSNtb+!IR+dPSf|PjjP5s*71`26cwFC`H|JaoL@uNT>0iAk5Sc8Wkqv-VJ9bdGGFw| zL=TD4?$*ZpLyjsgh)AA-WW^W;ILgSmrm>@wE;= zg+RhLo3Qp!N#bDe+=~i9a+Vh~8^CUpkIgAmW7<8(0uA3=9}l+y)s%h_Hgs4*7NklX zKKJLKLKDYfR#qWR2juo&6^;$hv%ZB5zu8j>6Kj@DSCdILOm5YzZZcSrH<5cWkcXG?l*6^xm0=g-^ zc@b4U7&bA8x;Aa~Qcwmn zNK9qU^8LOMhl`lasS8$t-OV?s{pOdPE{>jup$Ti{+AsEwrd~L>ou2NuZi8RjEGU)i zPX?^S#~!n@@Cibfq^kBPj{0e;wLg!>YCcPhp-z`baVOvbhU)(UmVDd$udu{8jzz5R z?Eh@)Ft2c`C~gYO`@GorAp=02+;FRp zX^{m3x55lk|Ml7_=xt=`Y+-=U4NO(Y{Y{nBB8}FWEHF_0O;hm_9kc%gT-S*$A1totdAq5K{_AvW2tDx!aUcHtf!sgrh3w* z#GbNd)1+A1)IY_IukA=n5!~!RyQD#22NE7wtkEbN8kc8d2?@~`gixG_PaGrj3y8pr z){c^HU#Lqikuc9B;cSg-vH?XKx9=AOKU=s31PqhAFYTA{?-S>Lva zDYZ!W3)=gt9tSBhSRYD%)(K;a^74W}qlxg9M(3~i&hhk)RjcFSr>(lq>*LUp zel`?Gg8QcUYO)^gI4eKh`ziWbn`YD1WREESA8&L7CaotqQ8TeySx8}qyyfZT(;$kK z8XoGjYKPV1mP47Pn*7hB?aQ?(r~>WhmLS(4`}n58ztIx!yMJg&o~lP`U1Z>8}9zwOm+DBXYI3T(}0f^Wv*QQceKP8 z1<;b8U6W~6V`>`1UM0nxBi-!5{YeW=m;;*^6wBT!0K0ZEEx3`>(csl&$=LWc9miN{ z3!N_kVo^99FNSzx8cFXk9(hvTP!Xb0ZBJWHu$Ju<(afhZ@Jt{p@#4m`j#}IK^eN{M z*9q>F9a+lE-E&6@yi_}k^R5GxzN!NXljx;EFPi}^^>RV;mc!FcwWnDrU;8zd!uM$g z3I>mIDX|hUkB~+C1_@IR&?9`eE#4b?qJkD@OeRyOfm3ItHVEtEL%NgQ4kFsyJm9j_ ztv{5+RE7_<=Rl(09&w~eWmP}<{6&wd%&=z9i6Jjl^|sAl6_fI>sGY(62Jv=fmCb1h z8mn)we7Tb(7K};5uJMXy68TxoSS@y7o!h12@@M#SY<#KFl(>aPHmzj+QSwC9&3pa6 zdh_T&wIuSq1Rqur=aa~dJKl;P{ME|yE?H~O>@<1rjOqzmfC_ItufmT8uNt=iu|Bva zM0h$|xbC*AUbP2hD6YDUN~i?yJr9fc4o{8ygi$O7Z=ac{>j@-*hHbX@ZcIbI4&Mg4 zZIX31b8bPB^BN}kAiY}f8P5v0c%mO%?Rx|uD~oIMSi?(Jfp&2f3!>`&6MML*0p5?` zvd}mBnBiO6HrOFptvs7YYdGv8NlqsWx%euER`zMNr%o8|mDe`g)&`i_7ItSXGJM3s zW^jyG4tRDUJb)V|Ics;a|1o_^Xh+ISm3)&nS{ z`|$)l@KlOWN~IqVlwkP2PQJ#8Y|ypMQ^^`g=uvn3uANv}^*)PQB&!A<;IFMIQ$AvS zYEDGK<_I27EeFY4vmKor#jHkqh)*Ubs`Avl-?I%~^jNjAos5Q0pr2sE1t(lElb;9d z?a}{SsL2U0#0ER`%tQ~i*3Gxl-@U+7tuO5ff+*G{Zswj!I^cJiOZ3}6yYWa)Xf;a@ zq}(!%4G6bAywiqNS5`k)9$MeOYJ>dX02^i%Aah!*&@fEW5-QlnEdSvU93TA+FDX?s zL*ig~08?hMGN8j<1^*A<$R6qjx|QKHB_6C2jv6ZGwds9!Tz&gb)Fdq_lUjm2E&DZT zAF^Bs6h|6Tk$p0HD_=47gTr&2Pf7j8Z^}{A(!G{p7U_;#Pg&lV3=J?b+@f9Oh%Et& zn~=a^j#h<{+;OpuTgVHdgGo5S`7*klkZ8Y(qx37}#=Q$E=Bub$3&To9nB4H{2uaTx z>@$tjk(M(kY)5jsX{uSqc@r&?4zv@d{7YrKx`UnRYF=9+QkpTjk4{o%cJr~Ke~Qy! zLr3dxmn~Ta7B%VT@(Qhc6pu(KQdOw4Szf5Q;ZFGUR6TZl8dWPO7?uRsZ0m1(|FPL_ zlB}yM*y#6{?m(P!Rw))efXc^su#QF(1NJH8(OHNNwYmakaEjIIdTg_;XGz5TvzX0V zL2fk!{g{$iM|!cm=si2}e?n{gud=3S11iFK=8=mO23Bg{8WwTFD6Oe^{RH3lK)Q51 z6t?D~c0FXM|YK$jcv7Aw}BaU&6NM{cXh`*RY z*e5_CThYvEg3&%TACtH(K5JfgU&5z)#x7R4f$@=l(};Q=gT3g$pM9=85Z?r-0jca* zZH!gx`-rvMuJrVWwbHxln}9`jhL^wwwA_L&DCa{L5G z&qENL<(wB9FY$p_C62UxHC8FxiG%0?szfDwfG z(?6CDKN9qiB2AM>^ElyJk~GkcRE4og4+gss{*<++ef~{nkScESM{#1xeFlG0TTfrf zLSIDL@#FKBYdlVLr(YvHz*R}U=mg3aae@k}#AP;&6Gz9TSh8Egz0 z!2@}i&5*;Gk&s!IzugFkKloVz+bSy~K2bB^_Bx`h-I?o)`V8#S+ZiCO>s5Yv`2+ET z-KisbE8PK#bIE}FKEMH2RY%zGD^Wcws4<5NTV3qSCBOUdwN-K*-Mju)=KMpDBsMv= zAvAAW9eO_zifJvLd^$+QtT;b>rc_NvcTESath3&3z2c;3WvoUdg;l(I@(Xq=jzQ?j zlfoh<>$s>4ylQwC)ZM2io6xhzq9(AV%#YOS6&CEHU@z@G9v*7?tPo^-XG~Te+$-PG zHLUDzB2VkX`N3;tlp@6Vb2tGH;%6+&-R|KX6$4K2*6RyJ_hin9t;LfLwV2Z7+=Maf zI(LD!{m2@&^3>M_#-M5?2uihUOy8Jda;eiOdeP;^RHflk-FgyJ3FceA-l`H1nV82@ zxA`^65!@Kf>P=+bOWr18qghKzlR8zz2v3s5=BirLWpx$OV$83Lc+;P#d_qjN?}EmB zuwn0mUGK_EI!B4mYDT1!uk#5w)>yS1zNw}4OEdH{DOK5HBqLIo+RC+5hAZ1-f1VvT z+_tq^*4)dxev-vp#oebla&o;1dw8&WSHAQ@x(_O1ygg-F-9`wcm<;L4fV6tG4{>uk zeAIk@NTgQGahj@j{QFe3BXuw$eZNX6E>9P7b954NBraZ0tQ3`Y=Er zqU!R;Rw79kiR@!5ZbBtYn!41m#G4^!%OEYqm8N&!CgX1eM2$IxaSQhS<_~m9){gOi z@dq!FPx@Y)ljCv|_go%exQ&u89ef&Q9xCOa=g8zrDx2mwY=%`>mt!iLV;gQfwwqtcH%=#QWQa2dO7@z&Z+|zfIvgUQSb?(_WY?)UDHZBTbe&1@| zK;n>9G#h;1N+B(*-rC#rBN!7RiVU!eI~F$;vQSA0_ex|*9VvMNL7SdUDT8T~g4*@{ zT{fh!2BGkP;CJC{fjMT<*j9@7EFiB~Q^!sLha%gEy|1Tr!0tvz9_~D#@L*SLQ_mKB zgB{&bi%EwGAFm2*s6|K{&S9>}mqYic3Umz+AnuL%7orLB@p5LwqUGxo!0P)958; zN^Crrcy7y0gQy4mMugR?1Czj*9@ED6Ve)-vN}NXNq_K*4^3b9yrzn8rhj-Y4{O*i~ ztnB0OK%pUX2O$W=0I9&<%1MFxM>^^aVb1*Qn0NRg}b=$57VimOG!AXqeXa8#d_`-QFxCMVZ!( zkUKWuS;20}0NW$=7Yty92e3_*upw*8n<34V*Ku@;U1T=$Y)HOKzZei?YdbU(K!8aq zFjYx=bc?ggw<`B=s3flYI1v05?%{e~`5%OP$isF2F5DAjM~_Ak@RB6!p@t~C*wj6a zwLFolI>KcV0`*~T4|v}6LmNuGXY9FH$eT-Z|4p)oF075$3&u^%K>SL=Si_QQ^Ysw# zqXb{W}ZSD!5N22A}q>sK2;>#Q_Wf1TV zrbL&e?$t=TVn)R_pc?~!oH)NO$gI_Yz+l1q?b?Qr0W6HCjh5VvQCY?+DR8+< zWAk(B>{H1Nu;wPT>m0N@K+E8(Y4_kRMtp+gz3O`?dgAGzk zPt;FRPR4b>cNZ1V?X@2I_pe*(JS}a}F72xt>d*AR&~pgA&kWS{Ir=KSVeKG~C^l&! zi{rRHs~ki*e=gSwOZV_Oau`g}00OPVXCM}l?zI|{Cw+ERH zHtc^L_87k{qj9WbR&bg9g)}@YTv{Ze(^FmBed=xH?y3O3kY>5`MJc15Im`e=rxE?N zp_XJ<)cq}LlWV%YSlW>IdqgZ9LfQ!-qIJ(jn+gyqEaDY4zhlrbP|nbJwo!03y_2O~ zCk-2U@44gLCTKZ@9T_^)Sh+XinS}_!1-+6epq#7a?fB5CS?YYVS&{#+tbM}n4f?3X zVwsA$%pfZ0L`Txj?*l5ZH=<~#+WD1~I(^@xZ-dR6xtIC}G~sz+;r1bQ3u(In4#x{o zlWQ39B|hvn^KOO-0dQ$*?0KkyMe8bTwZiFU?d@#pU!(9;Fjl}l>|6AE=QWGBp4|0) z{+(3bZ!S3QhC^h8y>c>X{EwdjfP~JgPy^9N5LN)IT24;`%=HrM)e1;jz7SDJ^?l;02k#@Q}~1@nlf}?JBhpvz`v!8U;S~GnucX zhla{!2+{F?PL*E^dv7@c-oH;TxJO#3nbjHJRQ+Dg7^2+l_NLERz3^zwis>MJD~p|O z;Q5|PvT%&HohUw*uBjJajpilq=Dy9ym&+upFRf|qmXjA=yUJ(?hphsv)^gJ-wu%$e zXJ>QW&Q&+`PB#*B#$9Q!>F&&>v&d={lFj3se|;WI#-ez9hiK9tA~sBi`*vS!svORv zeRUT8ZJze>Iu>n7qKir938ZKM`QzP97ec#uSxdaB7f7qJNu5tq|J1Lhss1P5Py}^v z9{|ab@&pH{8$U~|n$~f*$#hBWVBM&ivmlrA2r$U}x-uBUbSv=^D0_gH0fs`;HBMml z@>@~3W?}T`Kt!wVaj3?p>|e!d9iLkx)WtXBU0n=V=p$=|%2HZz=FJ}157A5p;V&NW z;t{_E{E(%pT2-5TDaOyp|7ml%sq|Xf`9Uy}P&&TzmMpB^cr^D?aYFaP!3*}c;r!k; zEERi;GYN?6%gdZVDJVWEfyIyq4q&Fee!!pDD6p#FR^fjs0vbO^jow+%SulnzkTG$r)$DXE9fj2h>Msl#pNhW5 z;#IBmZNH)65hOqDM)psRQ)fy{X-cO~RlGCA$M2nzw>dvFGr5>@S%F)6 zq*9Tkh?&7_-pACPvI^|o(O!Jk^6!;`we*z+AUu4Tbf@I>eQFnXaXr*?-Z*2SVo&!S z&+c(6BWm+1YhKFn^SgL^2K1U9f*vda#|xHy_zptSEip(*5BIc4`#_zE^b!aB4nQ)1 z-eE8~ipWYeUs_`kFsNJFg};{K0BTa|o*NhfYOrYT7%wFt>d0*p=|GCHzx zWD4!k+R$QdWQ-xTt z3Pen*GviyK?bdF6KOoh*o@*j(n)@cH2k$)= zI6fAphH85E^fe#Hi{1$Q-9s`E>%=#mV9qZS)U(7OcufJPhYA`9BMJH28dyI|@-K(_ zbQujFLmlh7&R=Z68(&dEU~?Yjb;?$8 z9W(b2a@>0_JX+AK(bKHa-Y%E^9_ll`JAIhYJR=6YQwAybRqz468)#%2oNxnFTbK zbS%%mrem|~)UB}LI_Q=jj+S>uTbuSMUObXYVoaXPt_=>W<9A{Jw4gqv$N-$-L+5<0 zv~%@BnGJ!-k`rQ`;ek29?pwf1M$~N{0@30BhvWo%@!ygY%WY9~h?OOa&r`h3g?M@% z>;$3xP@_U?$R#}<(?Tdw_gZEjKEhS#+|Y#jK-t!6|GX*0b}BKfcM@uMrstAnGo{@B z)?i;t_I#Kng9oebS#@wfm#Z--|MjVE0u|d%z?`zS!Sbo2P}k(88%jo;qR+oFR9<$6 zA(9Np%9U%3N+FIL5CbcjJh1Cn##1M3S9$L+?!(~H4|nSt61)2e0J zFuH{jougKx0=0G0SuQ19zpMBJ=yP&LgZ*NJHg&>(fdPK<{{RC%&UdFFiA$xTsyEus zlG!K3;hNCM@j$!58cDsH+X2Ki@MES*YS+}}D%#YBYUW0cq)*hA=`+O^9}$%o zjIYbxY4UfI_^d3(%+`joHb&f$0zu1z9Lfo6;1BNlsA!aX>735M^o4p%o%Fz|#nMIE zmOz${bZbk9$D=M6>b^vT{iX^CdGS!&z4F`jS^vhvJvaWt(!Q2;OXV)97Tt?YZ)x7$ zx(*Cbg}TWKSqBhpyXwu;BXO5$YmRpT((Z9dc3I`!~Ah{VQf z00_~ac(D3i=kMCa3+o8<)z%s7MG>#l+64#`E|*5wg)3MipvMRHZBV_}VT zaDi`^vlM@-5_b~(hSrVec{Ys#gT8_Y%QcKU!d+5CJovDp8i(YAJFo4Yn}O1HuAMZW zySs_d26C_n0jtb*+-~L)2O-OP77}2Ojoa9xObuRGd3!@z%l3_ueH4XLl99m7q|&}x zCn8iUnKG+*Pk8Zl2y^JkT#xah6~5-_36RHdkL;`zjpGA@voFhHS4cLfGkIN9k?N;L zwhdk$Sq|M=(gPLUQ)(U<2VD~{kd+Sgt*LeZq8Z5K5*W1u&CUqcB82I-vW@cF*CIA~ zQa@v1m~)JG7n!jvi~P)HL|VRd*E_gjhs+|@AB(=!tcArA4n+uaZ`^{3k<`Ik7)bi7 zysBQ32bMckL9hw{nb;6T54`%7w$Fw0{tsGSNEFFxV}6&@lW@Q3@il(@y0j-f|=R{!SXP{op6JhO7C| zkwZCSjLYNg8}|AwI}UGa%m^nrPhnMiq2tTl$&H-F4hkp>j7I_TV}52}D)9Peyv37| z({hhzZh#L@$XR$YEPS7cDss>P8tCz%qjT!XDx~VS5bv;zN}|w5F3W{QAfUzZ;WoOC z9-Y9EXB%O_G0m0%%*U@pgz_EFKZYrE(%AQ}!W2U_|C2BU{`!Gw7^$s zCQVka#12^&*}CPbThc44aJ2atmAax5d1ijbTgW*c`gHTgLk}0~e)22+nyx*(yja%uAu;^30p@+Sc{^H$4bJaxFw!Iw5BB$X-4^{ zA>R0>?M9U`r;&VcgDUjJZ`|qPSTs6@dWQB;Q7;oB0l(L!N*x{Qrj&+VYBGH2F z`PH-|7^4`s7PnM?MJEbcF&z{=rVJuFhb%r3)u1M*l7O-dg}Lv`X%|0m)L`@;7lsa( z8F^OxDY-hv&uFhA6l1EfY$Ixa!3rSH%ml~ZfRG1;zclMz>B)=W#qCkfghwbq#|#hc zGv7O=+>bhqc3v45f73*SgIBCGw>7iO;9&bwWc%!mI$Yu?!@zL*uJ!ok#YWgbVEMJT z*vkF+UYab-HqBN1pHOxbc1J@Iec2V0H~j2PRRcEa8z*A($Z zAKox;V7|^~lwD?uD!9mxX;X*0Qr9MugxC$8>!UX{LNOn+e*89aONTUIW{})B|NajT zDumfJJG?^HTVfU5RA%5qJpU_3QFuGxpBaTe=|3}yXCQ3q|EmZ^N}A}s`h;w_Nuu)nF$D)1%~yjQ6}>YfGR@-y!W=k2JcL8R>;v3mv+sm{O-a-dTCFJCbzMd zD0<}CiLZ{uxvSbXhpxTvo2gZv{r<_ath3WVCPMJbP%6XP3h^F##d9d+O0+iBO-MQp zY<`u_GhJ%`#V1tKzT+t9o>68DhX_WI`bUN<@rl9c=na+v6FQmX={bsnT&n4SlF?J1 z+~h|K?9D47t8|a^W@G{=IftqrtiTnUgE-4MgS_p1?Z0QGJAqC-G^o6@#*Ym6LwrteJI zcKtnqqnwGqMI(a5N~a5P zM>cYm&3WKn40)mbF|2{oiDjZjb_N%5&@kVSoK0m0LEc{I37=U^T-1hpnQ4fh!5g|Z z>w2n5#EmZ%rKyH=XDM>dD>R)%F)iv`-wc#ZU-t}h8Ebaah0sTmZI;qe%57Cpu}6tC z-^+xv-LIGTs+f^%a5de+x*y-9GfsfsPT3ADXxPKGDR|oD9$BWL0`Ds_ACV`KR#>b( zz&yHOj}M{9lbGD>bOEAd#(wH%94n) zCnKn7={e}L@;>KRj4BCtE`6Hoy2&bC_LVq-&u)~}t~`>gppaMmIj z0OPx*`MW7CtUA;eXiEF!V3udK@Q8QJy)Xm}u zCcvF~Rv&&ttOA;K-+Z`omRz3T{nV1~TwrpwoS={ePQQfUdrvQNW&au%FEd9LbWEr3 z5h_$j6MahZAy#y~A}(hP*Rt*paV46){`q3xn{@}87;eUfcZTz{cJi5p8x20eMLqjI zRN9rw!~|*6kPwULTy1dpc2dEfdrO~mm5|xsKt5WolDwY@*nk>B*p_)0x)m>xiycW= zi)Ie=uy?uLy%Rk38j<7lIsYSCMfh30kc^|#+m{1s_mx^*hP=-W_VNNfStECWqYUfM5DNE6?ezRd1|+X;FD zZ5txD89%H|ONMmw5~i`YGJnQVnOd2>$~XSwwIEwPkAZtq=ilQ_J;(z`L!2>EGk_(Zg;Hge@|TX4DdIq3Eal(vs;f zW*asG_o#}$a}IUK3A^26p(Ok)_ozDrJH61IQ-zVEl!Ix{{bm;B7tJH{ZjQ9BbjvFt z7(gpU;v)^7r;`*J^vqK7I)2{No3g_Qh_V4|kjbL^%Wez?eSS0H^pP6zLof`rExns5 zATG!9wm(_jS$BmHj?WUx5N46?bng76hJv1)w5x+^N#XjU7U_oW2<9<$>PC&%<>cCJ z${y;r|9hy2^vfvsNGGgY+yZ27qplnl*&%O-NU5`m{rkf7Hz76xPb?gG;9NADi)aD` zciLz|3oUR{xwnU979icXOJle^V$-G;;sxzr2*HFe1vay5934+y#MhU799mSx$TI71 zPt?5Hpq?i4XG1mS+(*{bJKLSz-{lg{@q)AMkAk^h55FGUxBOT#PddvEi}lpYeAn>U z>}huM@NBwWB{(Q}vn#}^lGQCXxA3WIq5>w(VeEO2Idm3ce)x#MD9TLq$6tS)sHyv= zzaW>$UAPk4_L#;}Oi4iN>OE=o{x6}_Ep?%SN=EuVQBwNaXuO_pgP1`D3vVREFo0#M3F`laE+jo}z=*_jJ6<6N(<+9MKRF&A6Il(1VY4zz94g{paS5 zgqMGHT8&N9i+EqZIY)3kjSRQ%aKqr`uzY2p#PT!$HWIl|6pPe`g#RsaCJ9wEPo%s(CxaeVWC5 zisS6ka``N4-!(}_{luf>?6mO`iSv>fc$R>RZyUgtetmiMom9`M^3c$!|o zcIN5M$pyYFGV9_O0KZL3@~>;Gw*Di86Lgjf})YsFV0i9-{ ztUYfI$yIzFMWM5RMb6%KKWwskt3@1NV4g?^HsIy2#qB#O(wWDp%>r zBpW^)0O*m--*`K(y>g!i>}$Tj)v~ z*y#PQ<36oY`cG?Ap0UMB`q!i(9p^o2bL1 zm53SMlEwC1U$b>K>TR!1`g^67M|bTb zb|xCqqvf7u7!y7ydcJGlyf#@G(fB;llnKyTnQ`~|cayBZqCM&M9AXFTQq`0qw>09N zPF}$ZexV-Yct$=z7L@V*B)eDqEB(yjxuwGyP)RZXk9OeEEWC)CuWHw(buS@OVRoI2 zFj6(d+;6Y}4Yc;zf5I5$=I;9_&?N}Da1yODy4Z}*O9D3^g6P%CxwVr^_KTuzonNee zj)w81{;eua_r}ptCCdPUU(uj54y+h4D#e*Nl|Jm4H-mqb*YGT5`Tv~PaACaxpdGaT zceKO*(Uv%1r0B`)H9ePL*R`2Fu!fUnGg=X{!)r;`St#H=R=^z{ISGE`qUSqOc9&Q& z0k?@*j0*{Og4=YflS2|f>c~PK-`!nKi-Ko2q+$o5uFVALT-XjrDGC{1BId5~ddXE@ zO92^--xt*)3jz-0(}#S?mm9s*qK^S1*CIH0h$pcnXkR*Hik1^|(Ur>AX)|P~FD><` z`*S*7&*v4FVD}L2zJpWKwf45>e{vcKoqWS#-nprZ7xcM4?>dj3t{8pE2SOXxLz9#* zPP32BY369coKY?M=0-n1L4-qnF#HW9+FWx&y44U-G9HbjfoZ+~=G^mw^9?fx#pq4TQX-$gj&3;#`ogG~rrj$#x;ww#)5UTh6Gu#Z)6 zciIIz`&YBad!=;Qc0?!d2T`9vlc!E+z*t!Jd#<5^ZWj3%_x0o6KOwQ-az9LH;sW_H ziWis|!8BVKO*N*>hP&r?UuQK2aSl}`Ft74XmJo%l2$Zy|+t2_Vjv~lh5eRVEfyr7MmLrVwZ&?~S%EYDkuz87FgNpkw$nrfxQh$)WB!#9 z)J#-;xz0*qre?9@^J}Q7zz_XHxV@D|i^sQnrXin>shjm|1(~3vIz5Zw9G@A|8SO)s zGH$lC-T=S1N1U2a&swoz@tBjAMNpIK&0J$(4^M4s;QHA3br%CvVC`v>`Napr=@b<3 zN%NDHCoraPy?@+nf5`_XD@%fd!>olbCfH-qDn)7OwY5gy_m?$0Lnz@((@Qw@ur~$w zzb|(1HB2ZFS@Eg)RNGm7tkr0|YE@zSVivVT1D>9^)`}1BnYq2j3hVGI!>wE*+t0EQ z=6y@q)QWo#iCDb7>hk|zW`hqBc(}Fkd4ZpW>HK4}=UVqlC{@I=L<$wiZM`bxm=9;! zGVMgGyzx%$on}WWv%{r4qLLKF#LT8z3!e+ktTEwILuYOlj{%;^5`)@?Tf;`~6U{R# zqFV8hu1xXP)k44AT9zN|zl6muRO`k^-|p`zV(*9{8-@$4JwtW%t4E$SWtfj?Z;{zD zmMWAp#8M#atW*W6xHrAI!BXOx*@@`|vGU|k&TH+-?gS*+0 z?JQ(lznKi%3+a?!Oa_+i$}_2Q)Hg$w&|$OJDp>vL+w~mkg8??ITsB0^L$4YC zJBNM^m&)+C3QE<8>I$>Clwkh3*UTI;1*u#I4dyQexz)dF;Pr`-?3eJGL|Ylsb@8)5 z?HSoE>d#9F8XL;lT^g7i7vrEL6YmIK`+{)btb)@f)IGvk$@ke)l|Q4MG!WgLMPkPv zm@l9xN9B*w-Dsa^ChoIsLM_sv4xoX~sh5LB%qG0S{mdG3+tWI9=Bz9MNW?kQYPBW} z>Z=vdPW0XD`#+eiWTbwXt?C@Lj> zZU1;-BHh&UuYrtr-Twf{(0uyKZiOTAm)#1XgG@-EiG+u=w7pP5vRhNS9M{TEtw|ZVLg1kJ{JG+q%bu!`+O(*FgH$$Jqh z;K}R1B*71V)}{BNM6dxhi#Zn43B@n!RNl|pt4&6*9xgS)zDV@NIXwhIP|7=~!xob~ zBc)K8#WhKwOJm70Lsp^y4`VyYT_xgzm<>s?az~Y#aZ+1gk(e(@4F|$9IPCmkDBX0r zgl8M2m}R~%szs>SmsZ8SDiOHKsfw)rmf6L}M1_+zTeeW8bd-EWUiOlm*Wu?Od#ag5 zuER{l?Ctv;rL>l^dwHnyu1pQg&*sDTc1$Re@95#K4NHw~Zc{}V>@Sa1V3p*=7vsB%>Yn93G#*j8>|>RrGzJrYRX}&;JHw6oo@RDQ@FR)8Bf2(>{tj zw#eG#8SSNkSwqFk_}jo>cR;4S9!@>c6Ev;aUbxK6S4i_T%Y`uLtTG_ENTN*|D$;B( zGx%cO7R4b!b(F-~{b?Z=j$T0dio*rEawFreb*ua?jzKd}^Ha9w=qkfVEM2A%w%Jia z2=Nn7;+yKhnTp{48625?3_8LGEri)QJuU6yUZ}|hx_*)llI`zjkUSjv%DY{tH>j&f z#Taa|61NF;uIwu49%_L-6Hzb35dkLSvs!=E{p1td+Wq*SLK%{ye*VUMN3xa*WmT01qWDztQJ&&9Wl{~^`kd>-qdmUCu7IqdVOc$3m7bTHpM{r_{O0n| zydAA-#9X19{4=UTPE@|q+q@!JQnRMp8hGztGl3aC;d?rsN}3s8M^jYz`*^)h2}XTJ zlYMoO6ZF$&C3nZ_<;G)brL~@@1KWZLVqwmaN0vBR1PG zUoRK?UhG}NP0bqoW~99HOz}(v)p4 zlwU*Qsbb|VAX)d^*!p*@Z0IWXRh&0V zef$PwarbVOOM~r0``;%PgMx!aQOh9ZlJV?D@~&G3&F$;Hd^WVVHtt#{TREJtz$_m< z(tA#7frF2G2bS7iTb@=Sa`+SzRir81dsU*OR&Qb?XDDq3F>f$h4|#9XtXNMjm;d!n zz14imXI$No67Kqu6+T`KJ?Mo4CF!*+Zk_$6TI02m=rF=Z_WY$MqMlwOHxT734?u~R zq_+_i)?8D5`{RSK4{TBD%C2UE6cI#tEQx4itf z{y9j?=xsn-@*m?$T^H|P_rSoH?Bo+Lb*R-@s&!YCc_SCp#1%*La7Fn3!X-P*bk6Y0 zU4xL8Bg$#1A-kFpnv#EExC%5cnKjJC7S`oVO{QtvS=Q7`IJRz@X-*c4p5(vkgH&eZ z*kBr#H-y4mY@gpqI_4mc6rXdz;>F|1i5t8)incbX^jSkPEqXkjcfocBoegjqDaiLH0Xyto*!OJ?3>=;O_#2o{NeNrBV8Z;)^R-6T`+ii;YA z8!WW3J^*iXi%zLR>4{NteU^UyM)0dp6oVPZ@N%0jf zPzV5`%wYW%*yUj~k5yTPd4@hTdy>0yGg38vMU(eYbxCc7Th%E`2J;am!L4eVZkNsv zW9NN~{KyfoyuCwY1?gyR35~vgjVujb^u{=h*XTTheH)3X+2E6bEgQYD8b= zDenFVFb`5@`OY<89av;7Qg(2dT2#@Js9zi-NW?@3$wYU&-c4_JWSh`~JEUn|s}W$V z^Q<#HRP5APXPFDW`Ld*<)|C!H``JNe-nC%(dOV&GFUn(9a+*tXfe@{mwq&A3*rmz@ zOvu1d>(uRbz2a-_Knzd!i;N2$}*X6J9m|75n(y#DS} z)Hz-h!zn4_1~^A4Zr@U8y_@a3!7MXcSmATpl{^C8MEfK!Ua@-?8IT8CjwZ@!bK;kn zTb0=Ym9HKbKR%|^!d-{PcAj&E(je-r+bJK%(jw`#?>NwE(Uw!0CpV(a#4?OiZTHV8 zTDXrn9jWujGhA^TPQ<$3Ju0nZ%YxZ^Kpti&k);?cp;JK_puhOZV|eGOJT#H?Z&oXZ zj$olt!B-rcgC$LQ6SvmHBFeakShVVm)+GN$93~BDcxByebybEnn=lMQ6%OEBJMZ8*uU-he{^no5cRd-1){ z8vYZ5_EWrF^wHB@`doY-%Wy6kX*8x{#sx-GkDt~L_6AAHuS95bTw0Dv@jTx?>m6Hd z@^eqGSkn0T4EC9|I9hfWJ+x8D*Yai~3Jb_*cSw#CE=#Q3P>LfoThvbHxIXmdo|;Si z$&@=Bj&ow;tSo!8>&OK9c<7Ps#deB-kWi@sy40AHb1)NOdC1;TwZF5m*as2`#NKJ% zx8V*<6WnHV+1R$2$@Y?mNT3acNu){sW9kZuhq}g2MF9oN)cUdgIS4{B(ezaqngz1d@6ESH;Hf z8x05}wyETaE+uJx{kEKtzauMu5KEkO(%Xw)cX;*j`sWhI{vQ^9{JG)pd-x^GN()Ts z+V>mhQ7Av5=afMXS@u6M>%tN-Cg|_}Lc>mYUAMjPP`S!%&3R8GZfsxfazp#m1=Zfq z!W7{CR;NGj#pJv8oISzz#sPx?wH%@>sB+_^Df~D_etvP2= zakNw819u!d{=WB|ugQ)E(;oQP^gUQTd5zoFuEq+hslgpKKG2X;rR>dW?3q+Y21sn!`j* ztjarEp$|QMUXXBp3_~LO!k7geS={u{`My2P6i_1_w&(Ub&&g0^9Cof+xI|8kH!s_1 zIHX9#BtIqN?z9N+S5*!G4RCiwG`$roQr2i6@Rov+0s=kAW(bfceyZPOa8C!V(fhvc z+lR#B{?nr;chJ1At9Ax(eMO6K#U{q^exM7}6%E$QGFtE>ed0`iR8sTxkp+*rwO+yh zL)?1?HPJSF-}E9~R6t5VR8XWVVCbN5s#K9CT}q^fUPF}`2~`O_^bP?j z2_>|I5<+?7dChe{b3gNZe`j_w`LMIuoy~Df6cr_WDvu^O1K}%5G2*mK8dK z@W?W0*qSIV(L=piBuudU&<|z;2E~x4>cvMZ&wsBM{|FWReI{sTLGZn8EbAjW>=*Ir zKF@N9^$A+eSrX9F|Hu0uGuxV%MUC}0HrED^Mm;D;+<2C36+C6T4`Gsb$6Yw%;_*Y= zyIvwt4SeSOZ){;=ooyIoa8m`$|a`{c+JsoIi;XET^peBsK`TUukRokNsM>_i3 z%f0>c)8x;exnwTD0zoz#mKA`C?l#5VX*+tsM%Q65V5-LeN8=~$r_2=N-80)Ay_>=;|DcykWrTs}>~J-NKHzl5qIp<_R!gTwM! zsJWI$oZ%$xPPx~>GXXeZ*-9_g4TkXHD519gInC1c->T z(XldJ%u4$MqC1siLK+Ux&75%G%K$#Z7-ld%~)obdrZGn zsL{1OZ@8rpnJm(Y0`=YHH0o)NN# z1q=r(>~D=zjYp^&YBZ?czuS8wg8g+{;-;z^`%AXn?>WxX-!@yMe^%i*H!R}SHyL>V zNS!!Mu7#BcaC^Y==(F2lmV~x82;oJd1!O;DaGiZ})rFat1n6ugBIBt}yGz={Fx9uW z0UVRHRnhUGajC7g=bp+f+P)#kl#X$kvw&)FlG33=VYk*xVBR;fT^0#&GOzeUmPF}) zHY+P~WgaV*q<8Q!zpsGADFVdt6m=5O+s0{7c^!+o=0+9_Kjl0~9c3?XJMLo7EGOot zHOMEGh();6J__?oe)=ntl{g88ms6cvx4b{nxUwv(7kxd=0dPht@dZYJrrsle%6-06 z%lpHk?;k-wt1tcqo%Xq`laKS0Z|VEfh3ZcwZg1m}Y9PGpVOvFR06^MRSwZ>!OTbs{ z`M`DNc=qmP1+|tGVl=DG)U)fNBe+qc8zv}_RP{T6eZIlc)VEg`BGeJhx6u-8r1tl5A%ROf3lCUG-9;v;bRUy-Dol-P9FZ>z?@v034XFD*?CTPKo7 zIRrjzhAMKhgnVPYzprg)3T9%EL0E?DCR}A00q>=ZWf<*SMwqNFs$M#jfnZ6wyMa{J&4hl~B&WLWU zbw3!VDn9$T`OGMV*L;|L(>({u{HQ;9e2G&za%T98QINIg`l7JmTezy<*)Ob4DadRQ zt!JT3U`H{=eH7;T6R*50lZ^?OE_e;`x5n0^Ean}@Y2YjuY2MO}jvmw~ITL_&qT{Ms zk0avS{+%R*H=t>Lh%gJIT!Pbl|MAn@v6bq<$mtrnAcz=uQ=U!F+F8v` zdPU^&lFQt4Zjt0tss+Ve_=`<2D)ZY9Ag%4J<@ofG+=T?fg0T=cm|`r%nwuNp=l-cO zaJO#LqnGv87sNiSP)56O^KgM>YPynv0oon*^RQL7CAXyN$kal#%VvadZ?(neaL4-S z^>{~4r33R!5U2M0m+85v054w=56^Ayg*hpD!TepL0-ty86wuM;7J5&-dhKvcz6oaY z?h|`iFx(fpbIpuCV1>dq_2A+b%J0&rdc2BB9pTRKn`=>CdVQf(sc|foT4&N&d+2dk z#6VYA0>U0-S)9Z%J^APSt(Tdz6kUMN=#fyv0m;!5|1qwihp=m@!d$4c_0Fxx?IKV- zI+UdpH(WMSkLSlg+3!(jdZykC6!5iF{MAD__suLe*jt|y;GJwC+u@#-*VZkPB{RPf zU)=Xui*S2rw<7j<`*;c}Hqnysc81x_+i;~Q%P47#ss)`O!LErf*w{=M%lE+DX7l#0 zsCft3mHJ_~im7`DOZ^G)kUI#r`ULfWDM`H)k%DC_;9R6NixqGW8O(06I~||Dzi)bv zYMN5x{ip#^saqJOIq&Rk_}u2tUQD`%h!DuWMZ#3}MfV~*xWCKojQuK_wseYBM%(SZ zfS|Vy8X+Z&ISp^nvnVzEWCCVvuNyWCaBjK2KxK%lTe;?)@-&g!XU8F9?987lY)(YH z(u9=J%0whdy~bp5dWjVu>9i0yzetj7T~5t)*eLBy9iDAI@<}Pu%l>U?eGpzH@0&YwDt79N@;Y3H?kvtmpOTW*iLz6 zGeJy_sQXaGmVWE?Fz@u=-ywsuq0zZs-%eXR{OA{WW-}c9EJf!7hXbjHZ%=>gDEY;U z+?~mX0|FG@y#fP9cY8!;9(cna?~r0ABP-9Hn*(Fl10Fw#b8m}pZt=PGr-KS+35UEswb1A7miITb08NRfhU4VXKpuJ#NgP4Yh2~z0LQH>xDgdw3&2o z5F`zwykr|IO3Vy<>1F(HZK+fQ0q;smKO874P<%Cn@6S6LT;2lD?AEN`{u;xWf;!>Y zX^d#S!){%{KxSJLrRQ(3rFRXxMmBqpFO#+KoS4wj>x zMm=MJOpfoY+|u{;n@OWGJ{P=!E*w`l&1r&qxbLUr|OS+D~Y3?G*bE;JWea9 z-xniW|D2uTz&#cKxpFRL`@l;d!z`rdqccWY?2MHx&l%-t{GK!zlRoTHj6VGsLi%nX zKmA5|Ca>U{i(hNt78$2EyT%Y(ceQ_W>WGE;+qe@i@eHnp&RINE{>G>irg}=RJO7|R>?$7F^gj7^5zP zUSXcfh${15i(G}Y{4tdUo;(zA+7^pky;<5)F8O9a8uO8QYK6W~q;h(0k-L%{EIQ97 zf;~A6tXQqBj0>bt0bkw1X!Q6%U6?*F2%g5-XVeePH{g{*o^N;Ax<*|iqk-lEHdkDO z*)-(A1%>@yoPb{VJuQ;~J><03?klfYtaCGvSHe0S>4$S=0yp2HxS7B27kS{GkJyyPU5v{8m|49CgdJYY zfm4Gm@aXWGbD9T1NaiSJvB;@?>Ompg6;y?IRoH^}K3t#Gxlio*8a`22pQk3De_;Jw z@tX>DPOa$zzHj$8dFMj%HV-2}yWn2s*5r)D{cMTy+doH>!!z{s0SjvWo5|6Vh3LVG z6zwtWP^@tjzm%gR8x`=aYR|2TCLcvSwWsGyf8|KfL^~7~7&ja_+@p*+zipb@YTd?daYt7AHY%{HMB8odR#sHv=-EAoIqlN!%Q}9(V!9_6o}arGig;`0E+0KX z3F%JM*t7ul(p`731hNz_!Q*Z zZE31&tjbayIR=tBeBq^!?BdboEjsVg^@Ikd8@oM5NA2GFLy->d0%wJcpgK~pp}G~x zk45px)|8d=8sL^w#i9r8umv`g-jcg}Xhj|CZcp^-E&sTU^Z$~>F+eMH)A!DLUnBKY zQvnnAb8m)=68C$8`52hc>NLyanjgF!b!=;vD_Obt4pm#_K`zG-Zs z_+?{|3%X5Vo}KmdLQ8L2K~39wx`gsB)H?8#wy{Bl<+v(Uu{dKP!|$u|tlMrEn?ky} ziv2S4vo`UHgA7RJW)&HXyJq&-sU?;o&MdV*<)>w#lZ5xp3hyE?j~R8(R-TkG) zx-znd4hp4PLghlg&vX!%Q_Y3L@e^$y#`~61;?dSdWNI6O2qfXW#!kQa>0UYW-q5vj z>yIS)$64Xt6nlLL`7fmt?CpTxE*$Bqe8?e1d&tGg-D9 zf%mAUYTT;(fa%uhke610q~r*Js8K5l<^3WdRykdTu;M4V9w>BOsc8_Wudeopn-jek z^6~Kln&bWR7!C^>GE3(Ub$!e!gq# ze)GJ!k<}}VryqO*G+T4VYfcYg~9gIz6Sj>+=PzP97aF{zaeapS_ zr%tmDV^i-z0gZPQwGHeXN}FmVUU@BTcw{+uGXM{v#iEAXE^{nhlX6c-a*AlvaxJQq zQfUSKL}@C%&hCfiS}B3J85ko4Q;j_#_ZX1rgA*OAx5?&s-((?4hwV??)Xv@;7R4sp zWWE060h{yR1~Nrz%0|}+>yF!j1cfPX z)vC5!EVS{?A=nt_D-UG&i45Ual>;)UrxDMu@RlPyX{*`GV4iSC=FiuZ)a@8``OVfz zpFd|AOxeLood~wL<2Jl|XZ(15EkYwqxsw_fGpVUX@EfzAhgy6J2t}5;)ZmE-- z9PfRAN7S2Bb@Adq4HUlCoK2@D2vu&4oIkzm&yx^_dG0f)8P{T$|9+o_dyj>CqnGH& zwT%wtG|{G*PBt^1d5S?B7gYAlC8J)>6J0Tdw>C6nLIpSGL{~dRiFGG zCmU?K8zz&>pR`vV_Uw4!F+!V)$vs@?109B1XrvpvIs?;rG0DXh2kSQKT-s>bv(A%p zc6qa<_f94#@A6_+O?O_KN$G{%>J~yR8?BoVKS|@H8d)}<4c3o%r^U~{oGg3m5XPA+#WP_rXN+nJ zv3q5KYdSzz9*{M^r1sID69)w_JWY*=dsYXe({j3;PXw%O0ry$u_651!y}wWNYp83} zC4S?@FvB7wz)9}#QbQa?ys^Z*=RB(BaIX?0f)4yintv^oPjs9=xbOwHCOEf?uN*)+ z-A{kCt~ftdT#-HqBqzyav!@T^oO|lbbu?V#Yt8GfI5BmqtW!!hS6=IInBw*=iJO;E zempMJJ1~P`BLe^QEnwD~!YctfTi{5KN(h8d*lWnWRky}8BkR}8Kg(`bYb_cz?c_S% zp|YdE%rpSsabMLfS7|pIW-vwwt<9fk(yMK^mb=ySkAxtbjqpwJ4k|i1UUsV^5-KB| zkOR+aYrkPU@oHU1r<|+z85c^EQ@=gsF}8g$S?8rQvq%?~vp!9w_gKnestP#<+_H98 zYQI(pD`0qxSeVVn?ymA4 z`g#|pX89s+F#$qXMh3KIbu5|OAnXFg)vjC|InZwPggVCOgL;)ZCAZL@9is`ja;Ca! z&!oq3??l*m5=$@RLL)2}{iby$mRh<#rF5scew)*VVq(Lk#7eRPbFSj5(!2^X#+9is zE@h{6TFmN0okPfPys_Vi&-1M%e}(Ah9xyUpdBTlB)```@SU|BWVN>(99i{*AxI#pe zuCkr}#jMT^+@lwZ>8@f@mO+5Q{8A3btgoCFk?Goux!nT-n|Dy9#uQ}jiK`Y4KJ=Rh zNv#5(E#YR3AvV4ftMwTjfWr|Am1K*-HPYibE*`>EvaZjUHZVVUJWA0WAv%PlVkMfS zZZj7<-I%>%NG~$T@dW2N;%HlqiGjRBtx4E39RjZ@0`_1k@)8D;P^;Z@Ac7Tn5& zgHcOWQeW?DR3o67_ z436lKIx63e&HnGgkuVq?$!IO}TzQAf`~GRfRGa|?n@VcabT_GIDASG#IOFHB?ecM^ zNE2aoRKdlpJCth)5NAA{%1zLW*$2{b{Tj*GUuw%W9O&Mn9kxp5S<+puXNtXaJ~!p) zWgPaHw&(62+~8Q-y6w51k2PQYRe$uJ_UM;9Q|fzxQfU1C+gBzQZ5k}^m7cx>vgW>l zzFrbG2wH()j;hXQ0^ryZa>`q4bnTNTCj;~g&5s5K?-hW~mWrjmlvbz^Lf*ShBW;v} zVJEM}T-FWw#Jj^+KV)1_v{!d|u5%nUGqWpxzo%z=B#vh1z|^cF^QmT@xuD}=2u{{J zI=({apYCFU!qKRz`PSQf4KYQZ0(d+&}6?7&nP_k8xM8B_YF zVD04<1_f$9ck0bIsa!d zcSOOg>E>>^So>zaz-NuDuf@@kc?-=KPKB2a|MZrX(RJ7IH7uBBvQ42C5b%UVj>K)m z*_M-%0fM)5hpY7sls?0sO5W3mW#WZk16j}+1otQ(K3v!5=bpN7`Uf?eoT4Y~Q+p&e zVE+U%rUNX)P8R6L_j|kxlDnb08Pt#&!5Q{X$Y1^k9BLzFJfzx9pha_{AXe`g{hIxALXu^q>) z=LfkAE;Jv+HjC8twdzETD+0n7pZZ@8zC8G19Tuu?`6X!Y(cv4cmJo%j^nn?y%l8X6 z*M|fZZ*UBkvGEn-GjFiQzr65%q|XRrTv`wYh8xJbFuJ%X1>nig>Im&ht`>O3&9kdB z0exgJ+UJV%Y!7X?IdC=37WJ}l@8oLl>L6unPog@^+B$)oi!-i_IgVFWhGIpH4681u zRl1>T$XT5`u_HG>CFfoiJ?Q7^bhm@=@8$g6Du4U#UG4ke65C~-wM+Pf2d(o5D_tQr zuf8lvp2*@HPtH$F&IpcI1BDv2|FXr#FUtFWMocVbRg}ZRaJ{PyqsFvi?W+kS#8AWk z9JA}zDiKvbqhuoaK;>#-rXQ@`fAZf?WBK=<{U5&h7gL6RQZ<(l`R@t+&ryEykuv`8 z9f|wiXQTzwsC;gULPy6dK5H3LxAMIe7aYsu*Ad7ZF3#sa1^7aQ!>rxp@neZ_rXNgQ zHj?Vm@xiILQ2*l+V<-9R63bHxvHpUdFw`Ei#U_h!fq(8c;n?M`zT)3SR|Xsm;P%gn z>jE60C9zM6YU85ut30b30f+2rh{#FG0GM6Gu)+a-hICfF1jP1O3IadZCnXbzI?DRY zc4{;ge2tZC3#(#S>*u4M3D=r@aC^#o;k}8y1zn;14c9Hxm%8mwE@`_y#68 zzOhR^i!ddo)a3H>WdC5E7&uBHb8P-oR6bxhuu7LJPdyW;QZxMx9X=DuPj)<@T1Kq+ z(KB%|6+3cPFthNF%XE==THFjKM~lr}UDj##DLUm7ppE{OlYN`}Jz`0NVr+^)RW^ZN z-*|4Lk5|k#x}W&3N4T9tkHnMLW;P@4*VnxOS^rHQ4*x~Y}_gNw}Ln`gGuAI6=cimg&SeOF!(t%&@elqw-`^I@{0IfMS|S+{kX65trA{s@eq2 zJQgUuxw(B#X?v}rCVVqR2^dBpQ+;@Rr1a%c3VwOLg;z2R7F5bmLIj2sGGJu7n=oly z>^%!WcZw~$Jm*WcH%LkKlmJ|nNkp53BN^9|VSSmWh<%m+&Rq3WibGbOppbERrm-99<2x*Es(~UW@KkZF@Wh=mB6(BNf0y^ zl0bg_~|~C0eHVwR4%kB7F3%`U69Tt7`kTQyk-TTT=gainOGbHv7^&0 z3m$=_ap{aEt-#ripuPbUTw5FTD1$9-ph2g4O6(W^g_Zupm{Gl zsOFCaYPtTRZ19oP6~yzgU`9S|T>lF`srsp)q@KEA|d#a}$M5Lzx$H0|r4@`<{ z-Hxg~TQWg|UKtQM_x@-%F0&mHb9piCe3|Y@Cn5AjGXN9d+1Vc!Z+>WZD^5|15ZuFH zyxBjG>(u7XQI)J1r}QFsskJz*Km#<}%0(8h6j0}S!78#MqN785X*pho=E`kv=BQnB zliSat9a10K?fxX}-Gvf(fS&`ag02=>ui~`!VJ(MCnm&L)Z4dKx#4AAapZ*Ah?BU?R zV2IRdCt!RF(ZC8@*`Bc@hs?Rzu42OpO9$G>qxH4Q%Pqfq+Xowtk-O(J@}2`mTQ=$T z@tJ`TdW&yXI441~893%*3NR9Uo~8ilnaZvh40>_JcJkyLei6u9NfO``8IN3FH3>9Y z4tdV}XL~CVp^iT_Q2;YeuY&c-7{})Er;-ZbVNL1Qlc-bQ1f~<|2VkZEMJ}I%(ALJZ z*UKFd5vK|Y%gpgU+-?aC+E`M!Fjr5bde*jwTvwFPp7!^4LP!WHkItF7g2TWuse)y~ zRId+nTSt^oMf?HjYSXZP;tSg#3oi49{1TOT$kww~J03cLadLTnx5Ib}S5J|>InLsjOa zM~4E>J+Kctsdw|^_gY^&yT-*$Z? z9w?O9pji7)6c;jVi%p?k?P`C6fR=iD45!Hi1Yb4;kDPw0RkRCrkyd~{&XDa(N24|a$wx=L?N8fzJaN)vL4`}sLjr}}B*R+MJ`K7T zuSY>NsX;5&xEe|4CBiYc^?~r4Q)9*8EA7oP`<4=jTw|g^lIg2vosk+c+xAR;ZI^&3 zg$vgo^@exSyjP>@q7L9~ZgB|F1_jggmJ2ujv;s4btsO_@V(!%s0w1J%OPW!=KtBj} z7k}`Zli$BTK(JlLBD2Lu#=CrQrx1U&M<(x4U_Sk_b^d(2Qlqf#>?T$kJ9h7_?Rr*Q z*p=Fw^-%s=SBHq zfi+x+>r6d?pnlRGCZ@7@>t<@-taG}*m;rF(mYx!7zq(3OoOr|e&Hu=gq ztM#0J9aon$5D}>OjNr?w=w=_u6IqU8`8GeAzi__f4v%Rcn}026xHPS|ziqq=o(r2&Df2X z&ufyoX{IOdNT<&=&pExJc3YR?VV|XuR>%H9ejWTCv1;1Bz^B=|`q<|9V`f35ThUHLqz*hTmD?Jp|t!w$c$rhCdR*P|Hb$Juzkou ztBdRS5F{(tP!7({sT2kBwUqSxlN+@BFrT6EqKG#C0dhi)`*kx#ZpQP^B&E+OZy2Kg z!u@yF4zXITmCvS|{;iQpGuQcBBUP+0tQbwKkrF%J!G}`g%;}+Y-<5L9w~l4BnbPVB z&k0i#A2sZlki4S$kb!f@P@XQMNROMHjN6akNEu?f3NzKfho`4nL?0haBBXf^mlPv7(;n}Uo`Ey&^Mpve~>AD3{2x%4qCTV@#7lXV^Ep#%AB z2AjFmYZEfS@{PgW*5+{M`NDT?j|K&5F>M^TFLIYT?3Hh_#|!IqMg2Vb}`~KL6O{?Ycf<9`IwT$-+7C zp>fuV@|@%V%12>tt<++kqtKNvijm~4MJi^z!R?}iSM8Guq3`ud?$1)i!#RcziSutr zs+2V67{SBiW1EdNFx7~`YD^H#-;ro3sej_de;f_kjaBe%Zia3>ppx-KwLb%?@_!W~ zWw=~vvXxi^sGz_~xXRyQ&(Gs%)$SB+i!1EM6fgHhz8ghye! zTwzAUR-lDy0_WY6f3`sn4X?*=L9S*#|LF1c>-TcsBIYKXYsmUp@wctgX!Z?yih-!P z)9WR6*O-ME(>w0P^ynAH=_mQUR9{l>+S;_clNYl(7g|_iru~4tLR#P6S^_j5!{{&* zN?Xc$yO1&Wa3ojTk$?j$S!W>krr`|FRoFbQ?9-{@td;+o9h-%aDVZy@$PlmgWTk_J z`|)|!0>YZCkxM4fJ$c6kJ7uCp_*`DlxkECL&&ni_ZKraF^yBO>4Ct; zKWaeeYTe#Ua#zN)83}Uf%GHh1)QPlU%{vxbs5KLbQ}zToiP)(%OD;eUn`AX+NQmURn>3yB3 z?22qRyXcT#K-?-tCeZfVD*reYUW<8*sWVV(It!s!N&VeRtjNO3l(jkz=)Vjwl{q#7 zxpXBGP#Fhh)4mlQFXd#9@z&7FDoa$oqR;x%8(fDDwt0NEhI2}xZd!F(2f!A!r*0msK0O`^AYMHr)YTEiYcvxpupY&$e2bM;3ZL8_38)&UfT8E?7~A<2AKmrH59} zFZ_3BZ0EE~7SrnOQKC;g_LCS9yOy~&-xTM0Bd%rED$~ph|;E{<-BLJh_@9~1xSD}{(lsojONb|>uX7E28C zyeB9oo|xYqI~KdpzKR&KPfb3%n>`Qi(%yU&7(>Ul(Yc(!R7WEq6aYACO!K33%2xx|}xYA}8|${(AFlj3L*o^KtHN zFsENdhfJvYoZE>~x#bE$S}n9o?P$o&F7}xFNcJuQ9kBTrHpY%8!v9AVnhYCGx8~k^ z#tO8&vse8O`i~K3gLxO#hPF4eI5%-{4KY4&dJ;}QDW%b%&3U$@()%y_f6)QdDX+QS zG5eMC9zrr7j_K+f?nWH|;mHE#8bSKyl$FYd4jKih$SmdLixJjv>Q;Ffid6Vg=Z4 zJuM(u|0@TyPL`OmDU-#y$7ijhH_gt>T%ScHyxHf=uqh&PQE_8E}LXbyEa`A zD}f`%&MG+cCJ)F*@lp-XHy3rn+V0jcbf+e)=#-v@f4XNR+^lOFe$H0Idop0!5QZa7)(rqi^hJ?{A&Dpt3%VWrbNF|KPG@tHxs)qINW*jzNHRL$$ zjUn6v*MH1E%BvnI;ux~9(&}(aPd|I3ymJfBBN&x-R4C|IK(5H0l!;CmlD3T-a(%b{ zP-b;v90+U^%i1{)`o|$q{zbca$X54Pw0fjtKc+CRd)f>xe&WT(4X?fyvUZkd+7gvD z?Waq2wo+xROB4c5oFkVSg^1!uhUsIcKiQ(U zvA&*=__nXbhX4=G7{c#YCw69CT@j=oM4tRr1`v4bXMh>vxw7)@%4pE-W^QI_peI<% zv-i+>P2JTu(bj#j+Ejw`KlT zoV9(Wf@s+p6?d$9JK9t#J1EAx|L(l?d(j&j-_`=5x;tC==8q|o1NT=JV7~d&pt?w3 z1mmFk+i34%_}5=$KpP*JyAqsfeJS>S-UFVp4`b&8>vXg1-ac+@khS;DD3W=-hpn`q zn1cvfe6OcGaYgBvEob-8Kkya?bMHM^PEfyHbr`P9&pA?=$SjjsKF+yZ!|-ilVV)A` zM1Dt#A;4niE4Z(Z-Y;y>sglZ1ZLupS>t{XG`O?&w@1H4+k<_F?DpFv2fM}#pO`B3= zlhn%VlK8f@{&h_#bvE()`O|G}){RNFeCVlFbk2@d3(=LdkG6TTH%IekY0fX>2i>pq z?ywOVa}$D7QoP38rf=BJb44xiGO0^Hh-^UlkJ zYKQN1@MoJWV%i!C;zdpywH*1S1G2ta#2x}#@I4l%kcuZ_SE)K@A-v@x`D8f(Iv~@pJ)HfC?Jj04_ozLEiHeT)X~HL(?>XBt5!Wq7MRs_^k|%!fJyd=SDzLt& zG}&Qq?w_uGEk+kS*ImW1FRCyvPIA?Z;se&}{lL<2S-OPnxh=bHuL-Z1%RV|LE&v3` zY%SKhEnVMWX{YnyZCpN)QW~R<;hUOuW+H(%Fg!}y#e6tZ$y%MeF7{E_1@BnpMZ4dS zmia2HxgPI><`}vky~G5-Fp9zh8+Z*=$cT%R2n@c^6TlE?DJyN1WY!j zFzHxreCE58#Y)wqEmy07PAO4?v$J}@^sJjRK!{-1525~?>Jw{UmmM8ss=DnNEy8S1 zMmzUnS{uOyO8$|O+AbUlF&a3voq`%nmqr~S#gnkPsijl1$+4>u2p<6CYqb!a2hBR# ziQGOvFogyc2>UqPss2V?&2CpDVaN2Amu31uTUhdg{y{mOUcdG)(Ta*6>zIP4!`zjQ z-DsMXz)05n?g#@?*7X=D8TT~u3BQ1eLG z!*=uNEsLVDPP5cw-sq865<>L>Cf%SU@=D9hvCyz->sHo1q!d}q?WTE?IbMDHMVdv|nbcbvdvPQ=m82R`+t zKN|V;{+WN3@@Xo!+=IfoO9Fouf-V+nh!c5g8^4{}L~T!=FKhKNMV=OTBl9$CiR*3n=f7jc9IeTs1Pe9EMpt-_?}D^amc(_Skd5{BvRO5&?k*gHzfeHMT7X$b4hQf#+px8tIx*}L^uE8m8ermtsq!xn103G>@l^Y7r#Y3zkug0q+X zrvj8S01#vR&2DFF?jBmN3f7L8IU1&@hJ5s@xcXk?%#P*bG7LMz?R>n}GB(HO%hRAJ za?6viv7)E0W?}m&0`$$k!N0RY!D5WFV$f;3&D|^0o4Wl3!w|6dl~Gc4;8B>P^?^KM z>`Q$*H$kZOm04N>0#>PZ;)VpFKvsSh#y@f(P*?$Ye1BlW0@3L4krycdvC_ZwgaB%8_6GI=aWB;Mqr)-UY0YW=C#85+L61Dv~J&&pc{XGaYvcpyF#W3_#J(u zIjfrLkA>;jMXK#CN-dvRe_nT2RsedW1!nx7&kuXZnb{2pw0Oj}+V@*i*3Fr}Vb>3# zezRZp>z*f}Z$Nb=AMnMiC|V1WG7o8X`rS<9b?<$PF;Ifsd3FLuhwZz;ZOpj<#0Ke& zD@ZhF7m2ipN(S%l7B2djXKAPDN@RU%(2qXTEQ74u+zNRN#4MR38Mq>jCWU=+Ogm~r zwU(buNLFMXLd?Q6`3>OaXI7lzo9Y*T@Bwhbtg`1F&3_>}8TV&vCA#h~t@4Id=wngj9mmzNXpb*uW5=#j=qY!JM4^@xpb&6=^&EG#dyA5L9 zdG;^nc9Hvg#0Pgf#NB@#oHITn<6>0~PQ92p7b1!a9F`m}HF-(nI6rbe=*Te?Ig`a7 z^-pfHK($&|{)^uEdGY_P_>%!E_47q>xyP&$B@H&IS{ZNe{7@{&ax7=0B}x_kB6cT= zYNBiTU!wniUWp{$8~lHR_zz-k|8Hnd_E%r=zc&r8>;I3u*FS&HFzOpE{cGjPu3zk&c4`Sj(<(<6Wc{ZL`L&~G}^fblZZ8-48f6g$Rv~jZ= z(1c3aa8+hDWnMPA3p|=k^qdX6M^Bwz6d)q6hG@5Go4K=Nel8&|r}(@S)RQW$@h5&1 zH?Z0Wml@BLiyW=7^6E6NFwUvbmQzIy6_#rNQem1%@nHQA9eq|8g?}g&z8z(Cg@*ta z5OBlE!q^D7)m$gMW@~(7Oq~%DL`cH3JvwlEd1XwwCRuUyM$YOI)9t1J-5*7@J;6Pi zZh0ShWl>c|K^g~3LBB#|N?JJGpx*pa7&o$0cMr%{zw$BU;*fc}#}J(qLuZm1Tb0rv zQklHla>ylbmS0xTt`lsLYiRy)TV_n|ahgKAy(LeeXJ&nQ=2Cc@!QVS{#>xbqAsq_b zxh=jMqUj0g1E7zToE4r4kg94hixod@9SB606ro7hQwr7D{`)G3T9K4|0KIk_0#)Pbsa`v_@^Sv79uAr#5%qRv4PiV-#H8F zLpA2EFZ&975N`*CbkE?|2J^)txqoOPTHXdctt6KFEjSrYYe`ScQC_N0Z%~b~(SmWb zV5974>i6u+SaRbrzbyu9J6nAq$WiL9c0d)3pP z57L7DgFOGua|Y{He-H2X^Y|q^;#hLSW>^+bCCbjckZz00iT*Ecl$6Sz zgc;lYWr=9-(x})$x5@>;IsHY$p5!zY{FaOmv9 zNBdkB*m<(sA0WL5*_R*kOI8$xpg#+-DeU!1ib|5tEmH?;lqw>%aFG?(X$&}fcycU) zbvFR62$@bv>^>_+C6~d|Dma?4)+Zyv*2Rf#1@iufN8*9T#NnOeX$5FFF(Im#4qVT0 z942835-p!DpI&^u)RhJAp~yoi2O%qI=Vl6RzoRG_jVSoqpbGfFr1smJgd+_Ef@6UkULy-f2DuhI#B7j;;?g7>;edo zmu7s@krU^6na)K2D`8gjlszvt%Dtn#SCldDlYhgJE>W8zyu~-j)bBX1*7il}(reoh z0BAToklq?!R=T?S#MDc>)=#S6rnuuvM7y4CP~j9$-6&oa&3i-P0@47r_ST;J`GDd}R(c0bxWagpTC*@OYvq`A$tCiDF2VVv? z`&?e&u=kZ{_+MYy#Y$GHaMw3D3vzsk-#sHwGY*GN|oWTR3OP!yD2q!X$T zrGrSXBAw8Ck)|R_l`g$VuZG^6)KEevlz{YJLI?qb6WsfKzwex{%$&@c49u+IuQiM3 zx$oz?eunj@X!wF}gE~_K)2M~_zGJeh-*N0fppOu~(e!UPOP@1W+)F7Cz0~iwCrW_D zQ_icKp#|=rf0>9x!B0ze76X5CNj^0C=2G-#!fPj;G<}fQ*HHv#niVmp z!h0#&8xTu56%^UlZ*q7jCwV7zlVMSMp`{&c`REpUuZTv;lxd#D4y7 z=Qi=F%Ab7iF6y>0r$OzAfuVbwQr|t(7Oi4SSx*{ms{>^O-h@eX}pxT0Ntx%*X;fhMv;wp+l=?=Lj|< zhwiVlSfj92=nm-cJK2no-)AVM$R}-C_U>aQgzn#O9s#BgTZOMA&+Z{q8+WmW8LQtN z?XgTPDSKDJDy_vF+Q3Rt9b-$GmI{uW=HnOd7i(BJ`IFQ9|a;T&== zrGFTXB5;JKwL4y>O* zhjfgw@={_0U+BiBuhM@OZSZyIuh$6@x3r$Ewm2)};$f(@6~~N+_h6Q&duwb8zGV%H?z*za_gxys?N0lv&mTCycIB z(?q7RUcQWjpnAqV+|o`2zs;MR|MH$+s0da^Vnlt2q7JPWZ?-Mb`K%JfvPo{$HYNVy z!V!Nt_6rNDg~oBGfG_U^)3|BWv(DY9YxS!#! zcW_GJ+k(u&BerldYI}YM^18Oi+h@9Jo(EBzr|_2F_mtSXaLZuLgzWf$LUPgYCpYQ2 z$m5HMPRW`)!qjw9{kEHSV`39Y|L*-HYUWCh%hQ=wd zRfevVVm4scy$-Cn@SvCMxV!%*wk>mQl1Gw-_G6V?^6D0S5P7Z4vTyDO0dw4Rs*KmGtNa-HeZ1@}WU)sPD8Lq+SV4)z3Mx8KhgUdCaAgQ3YyVM=zSW@ilV1;h~Uw%{H>b<^u&xwMaFJ!3mR*?><}JYeaR z58;Wfs@I+^XEDb<0!6sNDU-_$dGqad{Uqs`J{OX?1AqgTLsCl;zlsXT><5^tT?yye z7S?IMBYXn%Ca^76u>z?d<2W^FnVy*)|!HZ&f zT>aC@*%OK2^Eq5LFNQOWRGX`aH!oE!x1Y67W@>p02Tc8?HfyyclODsBws+Mu5C;Q_ z$ew$hc(aQiF_^O+lEY>?5m{@Z6O4+hqaT$o=+bAZRFb2G`bXfgf0eM9bfG0~N*0G( zg0Sie7dt_dH>}>Jp33-!C0CxQ<8YGb&iV^Z%0~jyqfAOB+#5)v(lH7 zSR+;GB}cLxc$_HUG_>g^_5*;VA*0-fQh7gkaoz;MfHjp2f&Vx3*F}S8?EZ? zrwEXgv_kQy2isk-0yBb?b(59L9x->?_)ws~{%n3|kntoIH=}h<{gGR{Dj~d<-AHg( z=RdmHm&<>5vj>qk$QTQ1Qa+k9M!wpFz(*g@sxZC;a1`*;zoCFtU_%3`Kh%BWx zn#H!TwDeoL+Z(G<>VbKDY6$LoReMaU+eE!LVqbiApYfa{l_XB&s>Ci<0b$ucgv3V` zL0mD~#F>7!Zdqw8riX^MRA#{|bEIN@`MbejLZnyt*A{i$8-AMf#|?_+u|Sd&rzxUv zp%TAfYLfNw{v$!bD|^uwQAspyX#IA$2yBq5L?Q?}Uq;*3R-|2K2>6J=cooka-G$^roPA zhDSPsl%b~IU7?7G2wohhJWj)KnC~UqvR_bGET8>FX{yO@cEU|W)lhTSoC7l#%C3ASH&P-Pt!650Gr`Fem;4-qcAX0RNUj1 zzant*1C4H9y^4G}fE}{9-z#lU6H5hJs-V^YEgha!W$kmlFKL}{g(VS|5KM@1+(<3m zQ|)J*d}&81B0kSdlKMm2{**@H<84#C9Yo+5&6Moa(s)PYU$Yqqzoo#CYs)M~o?hrm zkl9~&_dvh5&7GUYe`?862ia5hfm-Z;sW6>QXeku=JF7kN3AO<1u95t`wBzG~#Ds+G z(L(r7Y1MYo%xqBl65x__y(^Z&LK`1jZIeUpf0R%hMenleGW_6E5UPgT)~TGQ&%V=C ze9PHtu5-YBkk0H2zJcXA_rq0Xqb&D)BaF*rWa{LkRO4rPmC2!Us9y6tn{9l6-K=@& zi{;~YraXf}YNWK1yIIouwi*fbo+4}vqC;~9&le5U)ijduhNRAMhO3V;^Y?c4#QLDg zt}OEuDMs@n6NNV}Sg)fbUc5d$l3)hP;aDjBOIIwHgan+@ry%~>!t(or9r=hL-#rx@CGzn0dmh%r;$%46L#BFn$o&QiywKpMaAp4XiZyT z&PvZn{zopWSmx5#r%V@J#^)P;GBa$R@z!2)Z#n$9QpO-F)?e}Q5Zd; zb;7s6g0LT`4FTRs-&TRc+3LMeM^Z!F(T{bfuatU~F zM}xoN&xSX*LKsuNlXYWEX;O=RmgT%7ti6s7yTXV?5Q_?LnYAcpjNA>bOd?yvSdh zn^w0+j4#I#|CF4nqMNIf4PZZ<^P+tQUf*!}jw|3Gs??3b#@>}y^)CEm1Y%n;a!zkh z$n*>5sn3}`IP&L#Rp`r7*fC4Vt>UVXDmf1xVCKxwN_*KSG9WZpJ6cV2SXd~^*;(hl zXhGnO??|pJ0<}F~Rf>^gRywU~iuY;BHN~r33OichSsK?-wFHD#p@1Qp(H7Zo>X}if z*TlI2@!T4Sm-L0`Y9zc$Ws1~U}=EaKb1+o;d;Tj7rP$hZa6gaxO zE64rWn5_2M{}-DU*>#eBU4uFQ9R={Mi-*+~bLP>ZZ zu;YPN9zv0tSHe!&`aMvJNH-AVOqf^y;UD^S+7clG^JDuFurEde1+zjRPR z|A=KbDAsI}v5THwn>w_H{8OIzU^9RD{xq&c*1b5M+}-a3&$r=|qQOFD$$tKAN}&ez zSj@?={4p&4%PohWta*n&*kr^c4y7j8q@x>Z2Th(pUoi8Q_lPFfZy;4#Q%9kLH+*W= zHHRf+EWMgirmjvQxjB>oqvO?t5=!~vVs@jZaQGOP#KRKRs5!r^GVVQ*$%IzdeO-o^ zu_}%H8UDe}^x*OZ>;1~DzCK7}>IvN_k85dR)JMN_9=?k7+l6FX!d^X^xx-G|#g0af zgt(ou&=m&5DR4RFe%hf2Oz690U5l=5B;{OkRNU<3Bg^LryURRTqj%*>e9`7_3~M6&@zcnx6{U~F;xw7mU_&t=(_BJ{edYOX0vCGIEmwUb=v_^cM@NWs+#SU z`JM*dopW*Z?O}aJV|sEL?%%>4U}AW|aeQ zgZ`LFL+;?P!r;un_L^STGN3|LG>L+vV-J*f#`I_cr-Gz`LobhGldtb#4o~uggVxH) z{~)FBOBJv1z#v2ngNYZb@-0AULxuZIg6Goy!r;UZ>5S`lTsZwTv3LvjR~VfoS_xwC zdY+G%SbFr~UVt4>3ccW;2t^#2yXh89<6NuflUX?W*eGv0Dc`XLhqZDj?myDouFe5p z=l{eqxV|M?7N3y?%s9L+jjM^MKEViOl4u{bITP@$;f3-Z;>}iH|8;R< zrv9Hu&wq6_9bJdqsPcdP_CDl101uC5?d5YBt$%kp9bL@-6MOmZd;a<1e<_Ee-|Rg& z`?X97cR*>-rhiyA$pHg-nF!64Kc%C=Z&^t@x(?cMj#myt)c2k< znx~uxX*0Yrm##P$Xtg=ls12A`!?AO(+%futI`_%*prsG%_-lrtJ;ete;{ z7J?r#M)gfFql^axR?E;A%BX^S^rkn}JZEe1`O2LVMepi=Ynfp7w%Q6mg)9Qek2xZafJa! zj^w>l=}HV4Y;%|vj!L2YAi1?AY$4x(($xGvB^(fhJ)>>m?A(}9Z^%3=5p9^uxmsW+ zXBRM>I)0tjQeru$1kWkYRYp4W#Pn7dm8~a;BhURs#yofMZWGj?J{%)5)j%KV}tv_q~8}U=6Al(!OEAjWL9z9lZ`*@{M7WN@!IJuuk zRF!$d&9v#(ns$HOoGqB!Y!td(?gtLikARD(=*Optbhy$2!;PJ3bN8~48DUu$7$qcadn+e%X~8c^G! zi`T~pnY3DJziodo(|00QdV-#;-8tuCUnvEva$?L7j&#H=O8ZeyT)%Q`~58kV{3^Cs!%Dw zkTtAT+O{t1SbF-QbV(}JiLicRX3EbqUCEbfjqB+mgZ!&6yS{)}I|w?llFAXvvVe(| zx5P$#6ym-M$!$E0R-M~Lpq|xlAXyV~44P7FYTy2kB6gnB++bRa{5zQ^Y^26+;CMbp zjkNa3%%LPLrmj^#D%{swae07oz9FrrRjIHAZ~~JkE$^P%9NtUH-X1{iMd}SEMTQJi zgGPrFaPl?by`{ytKuztQna*WoO>AVQOqy9h8_b*geXV(NX%%St`Q;YU4OcFm?u0xeI~s(HZt3=% zFbF$6sm0`BLxm0&I;`szrfT;~?Rg9+LHENugE&cL89_b9E0rLjDho(Nq~j8w*q7&J zmjy$>H@?^LBmQ~PCo660 z>!Xx5-F~!u15(-Uz%~Y5t2~d1s$;MK>uO9b+!v26e$hnPo);CPm zaOualc%G&H`0*mz{qq^~<43i8@?(+8jhZGNI-^Tx^1n_RmUW^1HAvf6!u}CfS~>PZ ziihtwT=;D|HN4$Ea!WdgolGO26?+`JUv3rRS`5>-Qe6#A3g>HR+f}IB^Sj*_*lS2Vi6Yd!UxxY#Z>PY#d95E{1z> zr1N|*@wS+Vtj=-%Gi)WByef^M2kNu}f)3taQACr0dVW5kGa&G|!#HfyjN^c(4ue7P z2c=}BVv?no_&A-`>pfQwX~{d!+bsphC&6J!rIk*ps6p&&^KlB41rfWh+s~UV@(@lP z?nvcgw+GQS0>pRU5(C;P!ppPOYSP@KGjl30VY|b--qYr`M1{Xge}uN;j_!zOTI6>K z;S+9^4T_@}xT)8kQ~zRO0+F)a&AA3IU%Au&)!T$nt!&7W>{_`u z4hEB8M(ub?4lgEi&D>#1o;09~I7s8~tw&bHaYNtHoUx7@a$6nbS4%Y%Kc zPALofvlIxhAlMT%;whTYq!hvQ^k$+vDKZ7OuF8euyt1}m>wF3~Gx@xVZg@3%*sh3z zhx3W+9xf@W00z5D^F8%@yee*KiVj*;xr!>`Ko<#Gq;R7w4S?k#SPzGuc`+ua5_iv8 zSH$jrYnKZ3=&D+!eM-5r`m!9_ps6B}HgVg2$CN`DpBUhRZePE@K2@}B3)L8k<*2PR z>+LQP{dSLj>6TVP82?_LB|VFamV5JR5-6L=J1CyRO51w&BF1AP{i|Ff`&!qia*nWv zprrOpTZPutvURu@|jd-1GUnBw%D1(Dfcd#%DFOE^Yt~jdkYj3URbP&iNL&pzeFwx; z;$?JQ?L1{0B@X%d;8lAa2|#1Waj2GlnSvG&6<)tY;ZAmzpJSQ$IbO8Ib=LhY_?cTf zLiD$Ngo}S}H}~Q`xlr*}JGvk9lmjXG+#?P8E7X5YCPG6hd>oR32GVx&l`{R8=rMFk7`gb-y!ZePV+(GGoZ%A%DXlTwodq{Yam-@r zjPPPyCN!(`INl~?_X9tYlO!=rwAV;Zg_ZHlBfZ55*(%E4mxbHDD+xv~{fc{}8%`ZQ z%+CAlJHZZ-&@hG-w_fnROTgG~*8g#I#z{KRIrJq<`i(C^gU&yM4w$QZWBYL_|7_*f z&27Hwh&u!+>9p-OJt2o7yLd(afL4^+qNAgmmywrousg~5kHz8vk0VfAe4F+FiT19v zZ9M9u(G>#*O+FS{h3~4Er4gze(~yZ&HStfHZKV8Dz=eAoVeg)Gg{_M2Myp5p-_k)@ zkD}W*c{-f7>fczr-8lEuUT3w)BY<3I`(QN~?Z?BZUcw<=0gd94?J&YvK?Cj*A$Qh% z(p#ls?j;^3Dx0tQSj^CF0$G91^t*p&`}ah4R9+*J?#>A1gwP9_eut1Z^J}|+6|SQXj@-J+T+eHD>*fViImkZulZCAJgY<$Aq z5oo3SdM=yfXVOv;hT=8EsbLYoO*ClI8fyhapHKN;nPH+}hRIo^7VxyI9r*(5FWt z*rgfa5V5avhj0m?inElmblF>fh(XS<_}D#U{VbBNop8<}7%vb)!u>1Esoc7Lvnho| z7lH?FF#_PT^w0kaxYTRj8a8fR|0|Vg2>Lbt!FKL$*~wd+rL>?=I^$mK$6VOradKD* ze`$~EB05VaZ-beHGFMLgwA7rShv?f{m|0`dD5xh?-vZ9oo5i+q<|ComcYDa1CDSO2 zFDI2S?Os~_8ShZ)*~@cljpB;a%ZS6mCj&5ns(7>Yk^?6lSX;6c@YUhk(UUEozu-7m zX`7XfyCdlb$;Xu2_zVSJk+#Q(tWo02eywBbh|a!}%L!^?s5}B9Cxh_&aGCb5TsTisk^|ES-J6PHS?5K#Tsjy3*AuhUmkZ}| z$1Js@4Vplj-AE$eDjW}7x+E_4nz_mr-v~~b_%>mJh+wqs4QbEj{?{r!vRsR`HVg&i z_HXXkxyZg?iemoS#vRkf4?^XkKJVinmy=bP*ritI#OT?;+!_0EGP*b);pBRcn2lef z)n%n1p~X0;dv=JDvA^%W4P40fRco5JYT}G{N&oKN$LId06V4n0%?$lNeQP6b{SrH2 zWb}#TE(}b=JzuoGoyb0j}rmZq1_vs)pii(>mOJbhq+DrDu*E z)Z?N|&Dy5bF+;7CwuT1dAjjxgLe0=Rh5r|vmHrk2YW-M~?2tHFWLXK9)F zQ9!PF*`p-D$avAWf&+mn*E}unrkfNv`*kX0?D$M#m+~O-)Lh z(uUYK7AS}gj;}vw9vWM!H^se0rxq)@9+b)DWRiL9yb6=Cm?V^^Pu@(^vN>KBEmQXYfX_l1%q_5>3)cB?pjxR{OLYEJSwPOtHyC!G&Y?oymhdt8=>{-3w>)pjT0EbP}ok0CHA~nya&lSlPhSPW6 z00{E&alj<7GG*OVF$5esyX>8ja_!y;R;HZW6DA*C7vx>Q=l=?S2a~DGmMQ1wlxktG z`8MHRLu(Z*o`jpXFvQZN>nF#i_mQ4&?J(gnOb(JnP1#Q;!A$5XN59G7Ed@sBPDht`PuUT<0@fArwaV;TM7 zaAGDxv1g>CNnTZv-3WAU6T=gg-(KZApG75#+PI(s^{>nmvEs6Reqq#NB@+VG5h3Q$ zFMoJAL>>KpRd~}yB|w{&=G9%uZBEn0!=L^zEBE^Cvn2^SBX!(1P^2rEQZL2*?sLd( zPm&_Z@gkq95AUg;=5?mK8WCk_Qczko2_skBWYGI%Dh#|;u+*P&MTTqpwDl8Z{l?jY zK90JuJ!R7!uci@wa{{e-uy}ev%{j~)G7HkGC4k)V84nG0v2BdPm#uhyWn_7G_uXA5 z13&KwkKw_jVe&0Ib}uXG&{^vS0SXw4T{}LzHLhxr4M$@Y~csDPvRZ^e9Gk^oK{|}O@nZe z?~$KlYDwqah()o8cJ1Q^PaO>X`ZwUxZDuAZ*d{t6qWBxx7G}%F<}*5uuG&Pl%>bOc zylrUO5-z(>57K%so3|NZz~rh4yojddpN=oVk(amAbLIfFWBjQ0{e#|Mr_p4WM<3Af z9JNDCHQXD40&zMoYb8*xXReSG#%4_4*%4CgG?5R+4GP(D$b* zIP}Qqgcl1Vd`o;>T+VN+o|D5L{C9xMaCgMN02k9c$yf{9ut-cyi|Td;whi(p4L|=} zxc#|hbn9liMe3kuKIILufXc&$S7jT`DBLp<@9T@$yScx=b@k*ZZ}TK))gw89Zeyi1 z6p}bbv~L&!k$YzXzcVMU!{i;V{G%pAVZL*h<+B*$+Wam}aD!>byNUv`EYN6g?jvXy zmev320{#85J~(r3mm}8ajxod3_lMD3#wA7t)KQ=O*XE@VYITGzhbyHxk$lYWQh}C_ zXN##Jk;_!|eIEBV%C3y${a3oVg-r}uCB8bwsBSg69g$xga+n|mrKgzQm^Z3GUszoT z^7n4gTd&dO8JkALs z(G24{_rq!iYwWZ3>{7)9#AsWDyME>^^$jdefJ?YbBEj2zi8h?SPn#Qmhh^(u4{c-p z@&}P1B9B;5D2pcO*7;K&++~x^xpq-rPlID64(ISEVC=YW5%_OYBH>$;|LQ#quDBQT y?>nx$iN7!EP>F2C>mJ}gU;jV6#?6q8E0X6}E2&0$Co~@J^HN^*d6}$f@c#iRl!CSZ literal 0 HcmV?d00001 diff --git a/docs/assets/navigation.js b/docs/assets/navigation.js index 31eeb56d..79693c97 100644 --- a/docs/assets/navigation.js +++ b/docs/assets/navigation.js @@ -1 +1 @@ -window.navigationData = "data:application/octet-stream;base64,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" \ No newline at end of file +window.navigationData = "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" \ No newline at end of file diff --git a/docs/assets/search.js b/docs/assets/search.js index 29f32b70..bb7d8c6d 100644 --- a/docs/assets/search.js +++ b/docs/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file +window.searchData = "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"; \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css index 9d619a64..7f80f3dc 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -1,91 +1,247 @@ -:root { - /* Light */ - --light-color-background: #f2f4f8; - --light-color-background-secondary: #eff0f1; - --light-color-warning-text: #222; - --light-color-background-warning: #e6e600; - --light-color-icon-background: var(--light-color-background); - --light-color-accent: #c5c7c9; - --light-color-active-menu-item: var(--light-color-accent); - --light-color-text: #222; - --light-color-text-aside: #6e6e6e; - --light-color-link: #1f70c2; - --light-color-focus-outline: #3584e4; - - --light-color-ts-keyword: #056bd6; - --light-color-ts-project: #b111c9; - --light-color-ts-module: var(--light-color-ts-project); - --light-color-ts-namespace: var(--light-color-ts-project); - --light-color-ts-enum: #7e6f15; - --light-color-ts-enum-member: var(--light-color-ts-enum); - --light-color-ts-variable: #4760ec; - --light-color-ts-function: #572be7; - --light-color-ts-class: #1f70c2; - --light-color-ts-interface: #108024; - --light-color-ts-constructor: var(--light-color-ts-class); - --light-color-ts-property: var(--light-color-ts-variable); - --light-color-ts-method: var(--light-color-ts-function); - --light-color-ts-call-signature: var(--light-color-ts-method); - --light-color-ts-index-signature: var(--light-color-ts-property); - --light-color-ts-constructor-signature: var(--light-color-ts-constructor); - --light-color-ts-parameter: var(--light-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --light-color-ts-type-parameter: #a55c0e; - --light-color-ts-accessor: var(--light-color-ts-property); - --light-color-ts-get-signature: var(--light-color-ts-accessor); - --light-color-ts-set-signature: var(--light-color-ts-accessor); - --light-color-ts-type-alias: #d51270; - /* reference not included as links will be colored with the kind that it points to */ - --light-color-document: #000000; - - --light-external-icon: url("data:image/svg+xml;utf8,"); - --light-color-scheme: light; - - /* Dark */ - --dark-color-background: #2b2e33; - --dark-color-background-secondary: #1e2024; - --dark-color-background-warning: #bebe00; - --dark-color-warning-text: #222; - --dark-color-icon-background: var(--dark-color-background-secondary); - --dark-color-accent: #9096a2; - --dark-color-active-menu-item: #5d5d6a; - --dark-color-text: #f5f5f5; - --dark-color-text-aside: #dddddd; - --dark-color-link: #00aff4; - --dark-color-focus-outline: #4c97f2; - - --dark-color-ts-keyword: #3399ff; - --dark-color-ts-project: #e358ff; - --dark-color-ts-module: var(--dark-color-ts-project); - --dark-color-ts-namespace: var(--dark-color-ts-project); - --dark-color-ts-enum: #f4d93e; - --dark-color-ts-enum-member: var(--dark-color-ts-enum); - --dark-color-ts-variable: #798dff; - --dark-color-ts-function: #a280ff; - --dark-color-ts-class: #8ac4ff; - --dark-color-ts-interface: #6cff87; - --dark-color-ts-constructor: var(--dark-color-ts-class); - --dark-color-ts-property: var(--dark-color-ts-variable); - --dark-color-ts-method: var(--dark-color-ts-function); - --dark-color-ts-call-signature: var(--dark-color-ts-method); - --dark-color-ts-index-signature: var(--dark-color-ts-property); - --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); - --dark-color-ts-parameter: var(--dark-color-ts-variable); - /* type literal not included as links will never be generated to it */ - --dark-color-ts-type-parameter: #e07d13; - --dark-color-ts-accessor: var(--dark-color-ts-property); - --dark-color-ts-get-signature: var(--dark-color-ts-accessor); - --dark-color-ts-set-signature: var(--dark-color-ts-accessor); - --dark-color-ts-type-alias: #ff6492; - /* reference not included as links will be colored with the kind that it points to */ - --dark-color-document: #ffffff; - - --dark-external-icon: url("data:image/svg+xml;utf8,"); - --dark-color-scheme: dark; -} - -@media (prefers-color-scheme: light) { +@layer typedoc { :root { + /* Light */ + --light-color-background: #f2f4f8; + --light-color-background-secondary: #eff0f1; + --light-color-warning-text: #222; + --light-color-background-warning: #e6e600; + --light-color-accent: #c5c7c9; + --light-color-active-menu-item: var(--light-color-accent); + --light-color-text: #222; + --light-color-text-aside: #6e6e6e; + + --light-color-icon-background: var(--light-color-background); + --light-color-icon-text: var(--light-color-text); + + --light-color-comment-tag-text: var(--light-color-text); + --light-color-comment-tag: var(--light-color-background); + + --light-color-link: #1f70c2; + --light-color-focus-outline: #3584e4; + + --light-color-ts-keyword: #056bd6; + --light-color-ts-project: #b111c9; + --light-color-ts-module: var(--light-color-ts-project); + --light-color-ts-namespace: var(--light-color-ts-project); + --light-color-ts-enum: #7e6f15; + --light-color-ts-enum-member: var(--light-color-ts-enum); + --light-color-ts-variable: #4760ec; + --light-color-ts-function: #572be7; + --light-color-ts-class: #1f70c2; + --light-color-ts-interface: #108024; + --light-color-ts-constructor: var(--light-color-ts-class); + --light-color-ts-property: #9f5f30; + --light-color-ts-method: #be3989; + --light-color-ts-reference: #ff4d82; + --light-color-ts-call-signature: var(--light-color-ts-method); + --light-color-ts-index-signature: var(--light-color-ts-property); + --light-color-ts-constructor-signature: var( + --light-color-ts-constructor + ); + --light-color-ts-parameter: var(--light-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --light-color-ts-type-parameter: #a55c0e; + --light-color-ts-accessor: #c73c3c; + --light-color-ts-get-signature: var(--light-color-ts-accessor); + --light-color-ts-set-signature: var(--light-color-ts-accessor); + --light-color-ts-type-alias: #d51270; + /* reference not included as links will be colored with the kind that it points to */ + --light-color-document: #000000; + + --light-color-alert-note: #0969d9; + --light-color-alert-tip: #1a7f37; + --light-color-alert-important: #8250df; + --light-color-alert-warning: #9a6700; + --light-color-alert-caution: #cf222e; + + --light-external-icon: url("data:image/svg+xml;utf8,"); + --light-color-scheme: light; + + /* Dark */ + --dark-color-background: #2b2e33; + --dark-color-background-secondary: #1e2024; + --dark-color-background-warning: #bebe00; + --dark-color-warning-text: #222; + --dark-color-accent: #9096a2; + --dark-color-active-menu-item: #5d5d6a; + --dark-color-text: #f5f5f5; + --dark-color-text-aside: #dddddd; + + --dark-color-icon-background: var(--dark-color-background-secondary); + --dark-color-icon-text: var(--dark-color-text); + + --dark-color-comment-tag-text: var(--dark-color-text); + --dark-color-comment-tag: var(--dark-color-background); + + --dark-color-link: #00aff4; + --dark-color-focus-outline: #4c97f2; + + --dark-color-ts-keyword: #3399ff; + --dark-color-ts-project: #e358ff; + --dark-color-ts-module: var(--dark-color-ts-project); + --dark-color-ts-namespace: var(--dark-color-ts-project); + --dark-color-ts-enum: #f4d93e; + --dark-color-ts-enum-member: var(--dark-color-ts-enum); + --dark-color-ts-variable: #798dff; + --dark-color-ts-function: #a280ff; + --dark-color-ts-class: #8ac4ff; + --dark-color-ts-interface: #6cff87; + --dark-color-ts-constructor: var(--dark-color-ts-class); + --dark-color-ts-property: #ff984d; + --dark-color-ts-method: #ff4db8; + --dark-color-ts-reference: #ff4d82; + --dark-color-ts-call-signature: var(--dark-color-ts-method); + --dark-color-ts-index-signature: var(--dark-color-ts-property); + --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); + --dark-color-ts-parameter: var(--dark-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --dark-color-ts-type-parameter: #e07d13; + --dark-color-ts-accessor: #ff6060; + --dark-color-ts-get-signature: var(--dark-color-ts-accessor); + --dark-color-ts-set-signature: var(--dark-color-ts-accessor); + --dark-color-ts-type-alias: #ff6492; + /* reference not included as links will be colored with the kind that it points to */ + --dark-color-document: #ffffff; + + --dark-color-alert-note: #0969d9; + --dark-color-alert-tip: #1a7f37; + --dark-color-alert-important: #8250df; + --dark-color-alert-warning: #9a6700; + --dark-color-alert-caution: #cf222e; + + --dark-external-icon: url("data:image/svg+xml;utf8,"); + --dark-color-scheme: dark; + } + + @media (prefers-color-scheme: light) { + :root { + --color-background: var(--light-color-background); + --color-background-secondary: var( + --light-color-background-secondary + ); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-accent: var(--light-color-accent); + --color-active-menu-item: var(--light-color-active-menu-item); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + + --color-icon-background: var(--light-color-icon-background); + --color-icon-text: var(--light-color-icon-text); + + --color-comment-tag-text: var(--light-color-text); + --color-comment-tag: var(--light-color-background); + + --color-link: var(--light-color-link); + --color-focus-outline: var(--light-color-focus-outline); + + --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-project: var(--light-color-ts-project); + --color-ts-module: var(--light-color-ts-module); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-enum-member: var(--light-color-ts-enum-member); + --color-ts-variable: var(--light-color-ts-variable); + --color-ts-function: var(--light-color-ts-function); + --color-ts-class: var(--light-color-ts-class); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-constructor: var(--light-color-ts-constructor); + --color-ts-property: var(--light-color-ts-property); + --color-ts-method: var(--light-color-ts-method); + --color-ts-reference: var(--light-color-ts-reference); + --color-ts-call-signature: var(--light-color-ts-call-signature); + --color-ts-index-signature: var(--light-color-ts-index-signature); + --color-ts-constructor-signature: var( + --light-color-ts-constructor-signature + ); + --color-ts-parameter: var(--light-color-ts-parameter); + --color-ts-type-parameter: var(--light-color-ts-type-parameter); + --color-ts-accessor: var(--light-color-ts-accessor); + --color-ts-get-signature: var(--light-color-ts-get-signature); + --color-ts-set-signature: var(--light-color-ts-set-signature); + --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); + + --color-alert-note: var(--light-color-alert-note); + --color-alert-tip: var(--light-color-alert-tip); + --color-alert-important: var(--light-color-alert-important); + --color-alert-warning: var(--light-color-alert-warning); + --color-alert-caution: var(--light-color-alert-caution); + + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); + } + } + + @media (prefers-color-scheme: dark) { + :root { + --color-background: var(--dark-color-background); + --color-background-secondary: var( + --dark-color-background-secondary + ); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-accent: var(--dark-color-accent); + --color-active-menu-item: var(--dark-color-active-menu-item); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + + --color-icon-background: var(--dark-color-icon-background); + --color-icon-text: var(--dark-color-icon-text); + + --color-comment-tag-text: var(--dark-color-text); + --color-comment-tag: var(--dark-color-background); + + --color-link: var(--dark-color-link); + --color-focus-outline: var(--dark-color-focus-outline); + + --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-project: var(--dark-color-ts-project); + --color-ts-module: var(--dark-color-ts-module); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-enum-member: var(--dark-color-ts-enum-member); + --color-ts-variable: var(--dark-color-ts-variable); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-constructor: var(--dark-color-ts-constructor); + --color-ts-property: var(--dark-color-ts-property); + --color-ts-method: var(--dark-color-ts-method); + --color-ts-reference: var(--dark-color-ts-reference); + --color-ts-call-signature: var(--dark-color-ts-call-signature); + --color-ts-index-signature: var(--dark-color-ts-index-signature); + --color-ts-constructor-signature: var( + --dark-color-ts-constructor-signature + ); + --color-ts-parameter: var(--dark-color-ts-parameter); + --color-ts-type-parameter: var(--dark-color-ts-type-parameter); + --color-ts-accessor: var(--dark-color-ts-accessor); + --color-ts-get-signature: var(--dark-color-ts-get-signature); + --color-ts-set-signature: var(--dark-color-ts-set-signature); + --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); + + --color-alert-note: var(--dark-color-alert-note); + --color-alert-tip: var(--dark-color-alert-tip); + --color-alert-important: var(--dark-color-alert-important); + --color-alert-warning: var(--dark-color-alert-warning); + --color-alert-caution: var(--dark-color-alert-caution); + + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); + } + } + + html { + color-scheme: var(--color-scheme); + } + + body { + margin: 0; + } + + :root[data-theme="light"] { --color-background: var(--light-color-background); --color-background-secondary: var(--light-color-background-secondary); --color-background-warning: var(--light-color-background-warning); @@ -95,10 +251,16 @@ --color-active-menu-item: var(--light-color-active-menu-item); --color-text: var(--light-color-text); --color-text-aside: var(--light-color-text-aside); + --color-icon-text: var(--light-color-icon-text); + + --color-comment-tag-text: var(--light-color-text); + --color-comment-tag: var(--light-color-background); + --color-link: var(--light-color-link); --color-focus-outline: var(--light-color-focus-outline); --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-project: var(--light-color-ts-project); --color-ts-module: var(--light-color-ts-module); --color-ts-namespace: var(--light-color-ts-namespace); --color-ts-enum: var(--light-color-ts-enum); @@ -110,6 +272,7 @@ --color-ts-constructor: var(--light-color-ts-constructor); --color-ts-property: var(--light-color-ts-property); --color-ts-method: var(--light-color-ts-method); + --color-ts-reference: var(--light-color-ts-reference); --color-ts-call-signature: var(--light-color-ts-call-signature); --color-ts-index-signature: var(--light-color-ts-index-signature); --color-ts-constructor-signature: var( @@ -123,13 +286,17 @@ --color-ts-type-alias: var(--light-color-ts-type-alias); --color-document: var(--light-color-document); + --color-note: var(--light-color-note); + --color-tip: var(--light-color-tip); + --color-important: var(--light-color-important); + --color-warning: var(--light-color-warning); + --color-caution: var(--light-color-caution); + --external-icon: var(--light-external-icon); --color-scheme: var(--light-color-scheme); } -} -@media (prefers-color-scheme: dark) { - :root { + :root[data-theme="dark"] { --color-background: var(--dark-color-background); --color-background-secondary: var(--dark-color-background-secondary); --color-background-warning: var(--dark-color-background-warning); @@ -139,10 +306,16 @@ --color-active-menu-item: var(--dark-color-active-menu-item); --color-text: var(--dark-color-text); --color-text-aside: var(--dark-color-text-aside); + --color-icon-text: var(--dark-color-icon-text); + + --color-comment-tag-text: var(--dark-color-text); + --color-comment-tag: var(--dark-color-background); + --color-link: var(--dark-color-link); --color-focus-outline: var(--dark-color-focus-outline); --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-project: var(--dark-color-ts-project); --color-ts-module: var(--dark-color-ts-module); --color-ts-namespace: var(--dark-color-ts-namespace); --color-ts-enum: var(--dark-color-ts-enum); @@ -154,6 +327,7 @@ --color-ts-constructor: var(--dark-color-ts-constructor); --color-ts-property: var(--dark-color-ts-property); --color-ts-method: var(--dark-color-ts-method); + --color-ts-reference: var(--dark-color-ts-reference); --color-ts-call-signature: var(--dark-color-ts-call-signature); --color-ts-index-signature: var(--dark-color-ts-index-signature); --color-ts-constructor-signature: var( @@ -167,1282 +341,1270 @@ --color-ts-type-alias: var(--dark-color-ts-type-alias); --color-document: var(--dark-color-document); + --color-note: var(--dark-color-note); + --color-tip: var(--dark-color-tip); + --color-important: var(--dark-color-important); + --color-warning: var(--dark-color-warning); + --color-caution: var(--dark-color-caution); + --external-icon: var(--dark-external-icon); --color-scheme: var(--dark-color-scheme); } -} - -html { - color-scheme: var(--color-scheme); -} -body { - margin: 0; -} + *:focus-visible, + .tsd-accordion-summary:focus-visible svg { + outline: 2px solid var(--color-focus-outline); + } -:root[data-theme="light"] { - --color-background: var(--light-color-background); - --color-background-secondary: var(--light-color-background-secondary); - --color-background-warning: var(--light-color-background-warning); - --color-warning-text: var(--light-color-warning-text); - --color-icon-background: var(--light-color-icon-background); - --color-accent: var(--light-color-accent); - --color-active-menu-item: var(--light-color-active-menu-item); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - --color-focus-outline: var(--light-color-focus-outline); - - --color-ts-keyword: var(--light-color-ts-keyword); - --color-ts-module: var(--light-color-ts-module); - --color-ts-namespace: var(--light-color-ts-namespace); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-enum-member: var(--light-color-ts-enum-member); - --color-ts-variable: var(--light-color-ts-variable); - --color-ts-function: var(--light-color-ts-function); - --color-ts-class: var(--light-color-ts-class); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-constructor: var(--light-color-ts-constructor); - --color-ts-property: var(--light-color-ts-property); - --color-ts-method: var(--light-color-ts-method); - --color-ts-call-signature: var(--light-color-ts-call-signature); - --color-ts-index-signature: var(--light-color-ts-index-signature); - --color-ts-constructor-signature: var( - --light-color-ts-constructor-signature - ); - --color-ts-parameter: var(--light-color-ts-parameter); - --color-ts-type-parameter: var(--light-color-ts-type-parameter); - --color-ts-accessor: var(--light-color-ts-accessor); - --color-ts-get-signature: var(--light-color-ts-get-signature); - --color-ts-set-signature: var(--light-color-ts-set-signature); - --color-ts-type-alias: var(--light-color-ts-type-alias); - --color-document: var(--light-color-document); - - --external-icon: var(--light-external-icon); - --color-scheme: var(--light-color-scheme); -} + .always-visible, + .always-visible .tsd-signatures { + display: inherit !important; + } -:root[data-theme="dark"] { - --color-background: var(--dark-color-background); - --color-background-secondary: var(--dark-color-background-secondary); - --color-background-warning: var(--dark-color-background-warning); - --color-warning-text: var(--dark-color-warning-text); - --color-icon-background: var(--dark-color-icon-background); - --color-accent: var(--dark-color-accent); - --color-active-menu-item: var(--dark-color-active-menu-item); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - --color-focus-outline: var(--dark-color-focus-outline); - - --color-ts-keyword: var(--dark-color-ts-keyword); - --color-ts-module: var(--dark-color-ts-module); - --color-ts-namespace: var(--dark-color-ts-namespace); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-enum-member: var(--dark-color-ts-enum-member); - --color-ts-variable: var(--dark-color-ts-variable); - --color-ts-function: var(--dark-color-ts-function); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-constructor: var(--dark-color-ts-constructor); - --color-ts-property: var(--dark-color-ts-property); - --color-ts-method: var(--dark-color-ts-method); - --color-ts-call-signature: var(--dark-color-ts-call-signature); - --color-ts-index-signature: var(--dark-color-ts-index-signature); - --color-ts-constructor-signature: var( - --dark-color-ts-constructor-signature - ); - --color-ts-parameter: var(--dark-color-ts-parameter); - --color-ts-type-parameter: var(--dark-color-ts-type-parameter); - --color-ts-accessor: var(--dark-color-ts-accessor); - --color-ts-get-signature: var(--dark-color-ts-get-signature); - --color-ts-set-signature: var(--dark-color-ts-set-signature); - --color-ts-type-alias: var(--dark-color-ts-type-alias); - --color-document: var(--dark-color-document); - - --external-icon: var(--dark-external-icon); - --color-scheme: var(--dark-color-scheme); -} + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.2; + } -*:focus-visible, -.tsd-accordion-summary:focus-visible svg { - outline: 2px solid var(--color-focus-outline); -} + h1 { + font-size: 1.875rem; + margin: 0.67rem 0; + } -.always-visible, -.always-visible .tsd-signatures { - display: inherit !important; -} + h2 { + font-size: 1.5rem; + margin: 0.83rem 0; + } -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.2; -} + h3 { + font-size: 1.25rem; + margin: 1rem 0; + } -h1 { - font-size: 1.875rem; - margin: 0.67rem 0; -} + h4 { + font-size: 1.05rem; + margin: 1.33rem 0; + } -h2 { - font-size: 1.5rem; - margin: 0.83rem 0; -} + h5 { + font-size: 1rem; + margin: 1.5rem 0; + } -h3 { - font-size: 1.25rem; - margin: 1rem 0; -} + h6 { + font-size: 0.875rem; + margin: 2.33rem 0; + } -h4 { - font-size: 1.05rem; - margin: 1.33rem 0; -} + dl, + menu, + ol, + ul { + margin: 1em 0; + } -h5 { - font-size: 1rem; - margin: 1.5rem 0; -} + dd { + margin: 0 0 0 34px; + } -h6 { - font-size: 0.875rem; - margin: 2.33rem 0; -} + .container { + max-width: 1700px; + padding: 0 2rem; + } -dl, -menu, -ol, -ul { - margin: 1em 0; -} + /* Footer */ + footer { + border-top: 1px solid var(--color-accent); + padding-top: 1rem; + padding-bottom: 1rem; + max-height: 3.5rem; + } + footer > p { + margin: 0 1em; + } -dd { - margin: 0 0 0 40px; -} + .container-main { + margin: 0 auto; + /* toolbar, footer, margin */ + min-height: calc(100vh - 41px - 56px - 4rem); + } -.container { - max-width: 1700px; - padding: 0 2rem; -} + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } + } + @keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } + } + @keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } + } + @keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } + } + body { + background: var(--color-background); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", + Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + color: var(--color-text); + } -/* Footer */ -footer { - border-top: 1px solid var(--color-accent); - padding-top: 1rem; - padding-bottom: 1rem; - max-height: 3.5rem; -} -footer > p { - margin: 0 1em; -} + a { + color: var(--color-link); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + a.external[target="_blank"] { + background-image: var(--external-icon); + background-position: top 3px right; + background-repeat: no-repeat; + padding-right: 13px; + } + a.tsd-anchor-link { + color: var(--color-text); + } -.container-main { - margin: 0 auto; - /* toolbar, footer, margin */ - min-height: calc(100vh - 41px - 56px - 4rem); -} + code, + pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 0.875rem; + border-radius: 0.8em; + } -@keyframes fade-in { - from { + pre { + position: relative; + white-space: pre-wrap; + word-wrap: break-word; + padding: 10px; + border: 1px solid var(--color-accent); + margin-bottom: 8px; + } + pre code { + padding: 0; + font-size: 100%; + } + pre > button { + position: absolute; + top: 10px; + right: 10px; opacity: 0; + transition: opacity 0.1s; + box-sizing: border-box; } - to { + pre:hover > button, + pre > button.visible { opacity: 1; } -} -@keyframes fade-out { - from { - opacity: 1; - visibility: visible; + + blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; } - to { - opacity: 0; + + .tsd-typography { + line-height: 1.333em; } -} -@keyframes fade-in-delayed { - 0% { - opacity: 0; + .tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; } - 33% { - opacity: 0; + .tsd-typography .tsd-index-panel h3, + .tsd-index-panel .tsd-typography h3, + .tsd-typography h4, + .tsd-typography h5, + .tsd-typography h6 { + font-size: 1em; } - 100% { - opacity: 1; + .tsd-typography h5, + .tsd-typography h6 { + font-weight: normal; } -} -@keyframes fade-out-delayed { - 0% { - opacity: 1; - visibility: visible; + .tsd-typography p, + .tsd-typography ul, + .tsd-typography ol { + margin: 1em 0; } - 66% { - opacity: 0; + .tsd-typography table { + border-collapse: collapse; + border: none; } - 100% { - opacity: 0; + .tsd-typography td, + .tsd-typography th { + padding: 6px 13px; + border: 1px solid var(--color-accent); } -} -@keyframes pop-in-from-right { - from { - transform: translate(100%, 0); + .tsd-typography thead, + .tsd-typography tr:nth-child(even) { + background-color: var(--color-background-secondary); } - to { - transform: translate(0, 0); + + .tsd-alert { + padding: 8px 16px; + margin-bottom: 16px; + border-left: 0.25em solid var(--alert-color); } -} -@keyframes pop-out-to-right { - from { - transform: translate(0, 0); - visibility: visible; + .tsd-alert blockquote > :last-child, + .tsd-alert > :last-child { + margin-bottom: 0; } - to { - transform: translate(100%, 0); + .tsd-alert-title { + color: var(--alert-color); + display: inline-flex; + align-items: center; + } + .tsd-alert-title span { + margin-left: 4px; } -} -body { - background: var(--color-background); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 16px; - color: var(--color-text); -} -a { - color: var(--color-link); - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -a.external[target="_blank"] { - background-image: var(--external-icon); - background-position: top 3px right; - background-repeat: no-repeat; - padding-right: 13px; -} -a.tsd-anchor-link { - color: var(--color-text); -} + .tsd-alert-note { + --alert-color: var(--color-alert-note); + } + .tsd-alert-tip { + --alert-color: var(--color-alert-tip); + } + .tsd-alert-important { + --alert-color: var(--color-alert-important); + } + .tsd-alert-warning { + --alert-color: var(--color-alert-warning); + } + .tsd-alert-caution { + --alert-color: var(--color-alert-caution); + } -code, -pre { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - padding: 0.2em; - margin: 0; - font-size: 0.875rem; - border-radius: 0.8em; -} + .tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); + } + .tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; + } + .tsd-breadcrumb a:hover { + text-decoration: underline; + } + .tsd-breadcrumb li { + display: inline; + } + .tsd-breadcrumb li:after { + content: " / "; + } -pre { - position: relative; - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; - padding: 10px; - border: 1px solid var(--color-accent); -} -pre code { - padding: 0; - font-size: 100%; -} -pre > button { - position: absolute; - top: 10px; - right: 10px; - opacity: 0; - transition: opacity 0.1s; - box-sizing: border-box; -} -pre:hover > button, -pre > button.visible { - opacity: 1; -} + .tsd-comment-tags { + display: flex; + flex-direction: column; + } + dl.tsd-comment-tag-group { + display: flex; + align-items: center; + overflow: hidden; + margin: 0.5em 0; + } + dl.tsd-comment-tag-group dt { + display: flex; + margin-right: 0.5em; + font-size: 0.875em; + font-weight: normal; + } + dl.tsd-comment-tag-group dd { + margin: 0; + } + code.tsd-tag { + padding: 0.25em 0.4em; + border: 0.1em solid var(--color-accent); + margin-right: 0.25em; + font-size: 70%; + } + h1 code.tsd-tag:first-of-type { + margin-left: 0.25em; + } -blockquote { - margin: 1em 0; - padding-left: 1em; - border-left: 4px solid gray; -} + dl.tsd-comment-tag-group dd:before, + dl.tsd-comment-tag-group dd:after { + content: " "; + } + dl.tsd-comment-tag-group dd pre, + dl.tsd-comment-tag-group dd:after { + clear: both; + } + dl.tsd-comment-tag-group p { + margin: 0; + } -.tsd-typography { - line-height: 1.333em; -} -.tsd-typography ul { - list-style: square; - padding: 0 0 0 20px; - margin: 0; -} -.tsd-typography .tsd-index-panel h3, -.tsd-index-panel .tsd-typography h3, -.tsd-typography h4, -.tsd-typography h5, -.tsd-typography h6 { - font-size: 1em; -} -.tsd-typography h5, -.tsd-typography h6 { - font-weight: normal; -} -.tsd-typography p, -.tsd-typography ul, -.tsd-typography ol { - margin: 1em 0; -} -.tsd-typography table { - border-collapse: collapse; - border: none; -} -.tsd-typography td, -.tsd-typography th { - padding: 6px 13px; - border: 1px solid var(--color-accent); -} -.tsd-typography thead, -.tsd-typography tr:nth-child(even) { - background-color: var(--color-background-secondary); -} + .tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; + } + .tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; + } -.tsd-breadcrumb { - margin: 0; - padding: 0; - color: var(--color-text-aside); -} -.tsd-breadcrumb a { - color: var(--color-text-aside); - text-decoration: none; -} -.tsd-breadcrumb a:hover { - text-decoration: underline; -} -.tsd-breadcrumb li { - display: inline; -} -.tsd-breadcrumb li:after { - content: " / "; -} + .tsd-filter-visibility h4 { + font-size: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.5rem; + margin: 0; + } + .tsd-filter-item:not(:last-child) { + margin-bottom: 0.5rem; + } + .tsd-filter-input { + display: flex; + width: -moz-fit-content; + width: fit-content; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + } + .tsd-filter-input input[type="checkbox"] { + cursor: pointer; + position: absolute; + width: 1.5em; + height: 1.5em; + opacity: 0; + } + .tsd-filter-input input[type="checkbox"]:disabled { + pointer-events: none; + } + .tsd-filter-input svg { + cursor: pointer; + width: 1.5em; + height: 1.5em; + margin-right: 0.5em; + border-radius: 0.33em; + /* Leaving this at full opacity breaks event listeners on Firefox. + Don't remove unless you know what you're doing. */ + opacity: 0.99; + } + .tsd-filter-input input[type="checkbox"]:focus-visible + svg { + outline: 2px solid var(--color-focus-outline); + } + .tsd-checkbox-background { + fill: var(--color-accent); + } + input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { + stroke: var(--color-text); + } + .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { + fill: var(--color-background); + stroke: var(--color-accent); + stroke-width: 0.25rem; + } + .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { + stroke: var(--color-accent); + } -.tsd-comment-tags { - display: flex; - flex-direction: column; -} -dl.tsd-comment-tag-group { - display: flex; - align-items: center; - overflow: hidden; - margin: 0.5em 0; -} -dl.tsd-comment-tag-group dt { - display: flex; - margin-right: 0.5em; - font-size: 0.875em; - font-weight: normal; -} -dl.tsd-comment-tag-group dd { - margin: 0; -} -code.tsd-tag { - padding: 0.25em 0.4em; - border: 0.1em solid var(--color-accent); - margin-right: 0.25em; - font-size: 70%; -} -h1 code.tsd-tag:first-of-type { - margin-left: 0.25em; -} + .settings-label { + font-weight: bold; + text-transform: uppercase; + display: inline-block; + } -dl.tsd-comment-tag-group dd:before, -dl.tsd-comment-tag-group dd:after { - content: " "; -} -dl.tsd-comment-tag-group dd pre, -dl.tsd-comment-tag-group dd:after { - clear: both; -} -dl.tsd-comment-tag-group p { - margin: 0; -} + .tsd-filter-visibility .settings-label { + margin: 0.75rem 0 0.5rem 0; + } -.tsd-panel.tsd-comment .lead { - font-size: 1.1em; - line-height: 1.333em; - margin-bottom: 2em; -} -.tsd-panel.tsd-comment .lead:last-child { - margin-bottom: 0; -} + .tsd-theme-toggle .settings-label { + margin: 0.75rem 0.75rem 0 0; + } -.tsd-filter-visibility h4 { - font-size: 1rem; - padding-top: 0.75rem; - padding-bottom: 0.5rem; - margin: 0; -} -.tsd-filter-item:not(:last-child) { - margin-bottom: 0.5rem; -} -.tsd-filter-input { - display: flex; - width: -moz-fit-content; - width: fit-content; - align-items: center; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: pointer; -} -.tsd-filter-input input[type="checkbox"] { - cursor: pointer; - position: absolute; - width: 1.5em; - height: 1.5em; - opacity: 0; -} -.tsd-filter-input input[type="checkbox"]:disabled { - pointer-events: none; -} -.tsd-filter-input svg { - cursor: pointer; - width: 1.5em; - height: 1.5em; - margin-right: 0.5em; - border-radius: 0.33em; - /* Leaving this at full opacity breaks event listeners on Firefox. - Don't remove unless you know what you're doing. */ - opacity: 0.99; -} -.tsd-filter-input input[type="checkbox"]:focus-visible + svg { - outline: 2px solid var(--color-focus-outline); -} -.tsd-checkbox-background { - fill: var(--color-accent); -} -input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { - stroke: var(--color-text); -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { - fill: var(--color-background); - stroke: var(--color-accent); - stroke-width: 0.25rem; -} -.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { - stroke: var(--color-accent); -} + .tsd-hierarchy h4 label:hover span { + text-decoration: underline; + } -.settings-label { - font-weight: bold; - text-transform: uppercase; - display: inline-block; -} + .tsd-hierarchy { + list-style: square; + margin: 0; + } + .tsd-hierarchy-target { + font-weight: bold; + } + .tsd-hierarchy-toggle { + color: var(--color-link); + cursor: pointer; + } -.tsd-filter-visibility .settings-label { - margin: 0.75rem 0 0.5rem 0; -} + .tsd-full-hierarchy:not(:last-child) { + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px solid var(--color-accent); + } + .tsd-full-hierarchy, + .tsd-full-hierarchy ul { + list-style: none; + margin: 0; + padding: 0; + } + .tsd-full-hierarchy ul { + padding-left: 1.5rem; + } + .tsd-full-hierarchy a { + padding: 0.25rem 0 !important; + font-size: 1rem; + display: inline-flex; + align-items: center; + color: var(--color-text); + } + .tsd-full-hierarchy svg[data-dropdown] { + cursor: pointer; + } + .tsd-full-hierarchy svg[data-dropdown="false"] { + transform: rotate(-90deg); + } + .tsd-full-hierarchy svg[data-dropdown="false"] ~ ul { + display: none; + } -.tsd-theme-toggle .settings-label { - margin: 0.75rem 0.75rem 0 0; -} + .tsd-panel-group.tsd-index-group { + margin-bottom: 0; + } + .tsd-index-panel .tsd-index-list { + list-style: none; + line-height: 1.333em; + margin: 0; + padding: 0.25rem 0 0 0; + overflow: hidden; + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 1rem; + grid-template-rows: auto; + } + @media (max-width: 1024px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(2, 1fr); + } + } + @media (max-width: 768px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(1, 1fr); + } + } + .tsd-index-panel .tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; + } -.tsd-hierarchy { - list-style: square; - margin: 0; -} -.tsd-hierarchy .target { - font-weight: bold; -} + .tsd-flag { + display: inline-block; + padding: 0.25em 0.4em; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 75%; + line-height: 1; + font-weight: normal; + } -.tsd-full-hierarchy:not(:last-child) { - margin-bottom: 1em; - padding-bottom: 1em; - border-bottom: 1px solid var(--color-accent); -} -.tsd-full-hierarchy, -.tsd-full-hierarchy ul { - list-style: none; - margin: 0; - padding: 0; -} -.tsd-full-hierarchy ul { - padding-left: 1.5rem; -} -.tsd-full-hierarchy a { - padding: 0.25rem 0 !important; - font-size: 1rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} + .tsd-anchor { + position: relative; + top: -100px; + } -.tsd-panel-group.tsd-index-group { - margin-bottom: 0; -} -.tsd-index-panel .tsd-index-list { - list-style: none; - line-height: 1.333em; - margin: 0; - padding: 0.25rem 0 0 0; - overflow: hidden; - display: grid; - grid-template-columns: repeat(3, 1fr); - column-gap: 1rem; - grid-template-rows: auto; -} -@media (max-width: 1024px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(2, 1fr); + .tsd-member { + position: relative; } -} -@media (max-width: 768px) { - .tsd-index-panel .tsd-index-list { - grid-template-columns: repeat(1, 1fr); + .tsd-member .tsd-anchor + h3 { + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 0; + border-bottom: none; } -} -.tsd-index-panel .tsd-index-list li { - -webkit-page-break-inside: avoid; - -moz-page-break-inside: avoid; - -ms-page-break-inside: avoid; - -o-page-break-inside: avoid; - page-break-inside: avoid; -} -.tsd-flag { - display: inline-block; - padding: 0.25em 0.4em; - border-radius: 4px; - color: var(--color-comment-tag-text); - background-color: var(--color-comment-tag); - text-indent: 0; - font-size: 75%; - line-height: 1; - font-weight: normal; -} + .tsd-navigation.settings { + margin: 1rem 0; + } + .tsd-navigation > a, + .tsd-navigation .tsd-accordion-summary { + width: calc(100% - 0.25rem); + display: flex; + align-items: center; + } + .tsd-navigation a, + .tsd-navigation summary > span, + .tsd-page-navigation a { + display: flex; + width: calc(100% - 0.25rem); + align-items: center; + padding: 0.25rem; + color: var(--color-text); + text-decoration: none; + box-sizing: border-box; + } + .tsd-navigation a.current, + .tsd-page-navigation a.current { + background: var(--color-active-menu-item); + } + .tsd-navigation a:hover, + .tsd-page-navigation a:hover { + text-decoration: underline; + } + .tsd-navigation ul, + .tsd-page-navigation ul { + margin-top: 0; + margin-bottom: 0; + padding: 0; + list-style: none; + } + .tsd-navigation li, + .tsd-page-navigation li { + padding: 0; + max-width: 100%; + } + .tsd-navigation .tsd-nav-link { + display: none; + } + .tsd-nested-navigation { + margin-left: 3rem; + } + .tsd-nested-navigation > li > details { + margin-left: -1.5rem; + } + .tsd-small-nested-navigation { + margin-left: 1.5rem; + } + .tsd-small-nested-navigation > li > details { + margin-left: -1.5rem; + } -.tsd-anchor { - position: relative; - top: -100px; -} + .tsd-page-navigation-section { + margin-left: 10px; + } + .tsd-page-navigation-section > summary { + padding: 0.25rem; + } + .tsd-page-navigation-section > div { + margin-left: 20px; + } + .tsd-page-navigation ul { + padding-left: 1.75rem; + } -.tsd-member { - position: relative; -} -.tsd-member .tsd-anchor + h3 { - display: flex; - align-items: center; - margin-top: 0; - margin-bottom: 0; - border-bottom: none; -} + #tsd-sidebar-links a { + margin-top: 0; + margin-bottom: 0.5rem; + line-height: 1.25rem; + } + #tsd-sidebar-links a:last-of-type { + margin-bottom: 0; + } -.tsd-navigation.settings { - margin: 1rem 0; -} -.tsd-navigation > a, -.tsd-navigation .tsd-accordion-summary { - width: calc(100% - 0.25rem); - display: flex; - align-items: center; -} -.tsd-navigation a, -.tsd-navigation summary > span, -.tsd-page-navigation a { - display: flex; - width: calc(100% - 0.25rem); - align-items: center; - padding: 0.25rem; - color: var(--color-text); - text-decoration: none; - box-sizing: border-box; -} -.tsd-navigation a.current, -.tsd-page-navigation a.current { - background: var(--color-active-menu-item); -} -.tsd-navigation a:hover, -.tsd-page-navigation a:hover { - text-decoration: underline; -} -.tsd-navigation ul, -.tsd-page-navigation ul { - margin-top: 0; - margin-bottom: 0; - padding: 0; - list-style: none; -} -.tsd-navigation li, -.tsd-page-navigation li { - padding: 0; - max-width: 100%; -} -.tsd-navigation .tsd-nav-link { - display: none; -} -.tsd-nested-navigation { - margin-left: 3rem; -} -.tsd-nested-navigation > li > details { - margin-left: -1.5rem; -} -.tsd-small-nested-navigation { - margin-left: 1.5rem; -} -.tsd-small-nested-navigation > li > details { - margin-left: -1.5rem; -} - -.tsd-page-navigation-section { - margin-left: 10px; -} -.tsd-page-navigation-section > summary { - padding: 0.25rem; -} -.tsd-page-navigation-section > div { - margin-left: 20px; -} -.tsd-page-navigation ul { - padding-left: 1.75rem; -} - -#tsd-sidebar-links a { - margin-top: 0; - margin-bottom: 0.5rem; - line-height: 1.25rem; -} -#tsd-sidebar-links a:last-of-type { - margin-bottom: 0; -} - -a.tsd-index-link { - padding: 0.25rem 0 !important; - font-size: 1rem; - line-height: 1.25rem; - display: inline-flex; - align-items: center; - color: var(--color-text); -} -.tsd-accordion-summary { - list-style-type: none; /* hide marker on non-safari */ - outline: none; /* broken on safari, so just hide it */ -} -.tsd-accordion-summary::-webkit-details-marker { - display: none; /* hide marker on safari */ -} -.tsd-accordion-summary, -.tsd-accordion-summary a { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - - cursor: pointer; -} -.tsd-accordion-summary a { - width: calc(100% - 1.5rem); -} -.tsd-accordion-summary > * { - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; -} -.tsd-accordion .tsd-accordion-summary > svg { - margin-left: 0.25rem; - vertical-align: text-top; -} -.tsd-index-content > :not(:first-child) { - margin-top: 0.75rem; -} -.tsd-index-heading { - margin-top: 1.5rem; - margin-bottom: 0.75rem; -} - -.tsd-kind-icon { - margin-right: 0.5rem; - width: 1.25rem; - height: 1.25rem; - min-width: 1.25rem; - min-height: 1.25rem; -} -.tsd-kind-icon path { - transform-origin: center; - transform: scale(1.1); -} -.tsd-signature > .tsd-kind-icon { - margin-right: 0.8rem; -} - -.tsd-panel { - margin-bottom: 2.5rem; -} -.tsd-panel.tsd-member { - margin-bottom: 4rem; -} -.tsd-panel:empty { - display: none; -} -.tsd-panel > h1, -.tsd-panel > h2, -.tsd-panel > h3 { - margin: 1.5rem -1.5rem 0.75rem -1.5rem; - padding: 0 1.5rem 0.75rem 1.5rem; -} -.tsd-panel > h1.tsd-before-signature, -.tsd-panel > h2.tsd-before-signature, -.tsd-panel > h3.tsd-before-signature { - margin-bottom: 0; - border-bottom: none; -} - -.tsd-panel-group { - margin: 2rem 0; -} -.tsd-panel-group.tsd-index-group { - margin: 2rem 0; -} -.tsd-panel-group.tsd-index-group details { - margin: 2rem 0; -} -.tsd-panel-group > .tsd-accordion-summary { - margin-bottom: 1rem; -} - -#tsd-search { - transition: background-color 0.2s; -} -#tsd-search .title { - position: relative; - z-index: 2; -} -#tsd-search .field { - position: absolute; - left: 0; - top: 0; - right: 2.5rem; - height: 100%; -} -#tsd-search .field input { - box-sizing: border-box; - position: relative; - top: -50px; - z-index: 1; - width: 100%; - padding: 0 10px; - opacity: 0; - outline: 0; - border: 0; - background: transparent; - color: var(--color-text); -} -#tsd-search .field label { - position: absolute; - overflow: hidden; - right: -40px; -} -#tsd-search .field input, -#tsd-search .title, -#tsd-toolbar-links a { - transition: opacity 0.2s; -} -#tsd-search .results { - position: absolute; - visibility: hidden; - top: 40px; - width: 100%; - margin: 0; - padding: 0; - list-style: none; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); -} -#tsd-search .results li { - background-color: var(--color-background); - line-height: initial; - padding: 4px; -} -#tsd-search .results li:nth-child(even) { - background-color: var(--color-background-secondary); -} -#tsd-search .results li.state { - display: none; -} -#tsd-search .results li.current:not(.no-results), -#tsd-search .results li:hover:not(.no-results) { - background-color: var(--color-accent); -} -#tsd-search .results a { - display: flex; - align-items: center; - padding: 0.25rem; - box-sizing: border-box; -} -#tsd-search .results a:before { - top: 10px; -} -#tsd-search .results span.parent { - color: var(--color-text-aside); - font-weight: normal; -} -#tsd-search.has-focus { - background-color: var(--color-accent); -} -#tsd-search.has-focus .field input { - top: 0; - opacity: 1; -} -#tsd-search.has-focus .title, -#tsd-search.has-focus #tsd-toolbar-links a { - z-index: 0; - opacity: 0; -} -#tsd-search.has-focus .results { - visibility: visible; -} -#tsd-search.loading .results li.state.loading { - display: block; -} -#tsd-search.failure .results li.state.failure { - display: block; -} - -#tsd-toolbar-links { - position: absolute; - top: 0; - right: 2rem; - height: 100%; - display: flex; - align-items: center; - justify-content: flex-end; -} -#tsd-toolbar-links a { - margin-left: 1.5rem; -} -#tsd-toolbar-links a:hover { - text-decoration: underline; -} - -.tsd-signature { - margin: 0 0 1rem 0; - padding: 1rem 0.5rem; - border: 1px solid var(--color-accent); - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 14px; - overflow-x: auto; -} - -.tsd-signature-keyword { - color: var(--color-ts-keyword); - font-weight: normal; -} - -.tsd-signature-symbol { - color: var(--color-text-aside); - font-weight: normal; -} - -.tsd-signature-type { - font-style: italic; - font-weight: normal; -} - -.tsd-signatures { - padding: 0; - margin: 0 0 1em 0; - list-style-type: none; -} -.tsd-signatures .tsd-signature { - margin: 0; - border-color: var(--color-accent); - border-width: 1px 0; - transition: background-color 0.1s; -} -.tsd-signatures .tsd-index-signature:not(:last-child) { - margin-bottom: 1em; -} -.tsd-signatures .tsd-index-signature .tsd-signature { - border-width: 1px; -} -.tsd-description .tsd-signatures .tsd-signature { - border-width: 1px; -} - -ul.tsd-parameter-list, -ul.tsd-type-parameter-list { - list-style: square; - margin: 0; - padding-left: 20px; -} -ul.tsd-parameter-list > li.tsd-parameter-signature, -ul.tsd-type-parameter-list > li.tsd-parameter-signature { - list-style: none; - margin-left: -20px; -} -ul.tsd-parameter-list h5, -ul.tsd-type-parameter-list h5 { - font-size: 16px; - margin: 1em 0 0.5em 0; -} -.tsd-sources { - margin-top: 1rem; - font-size: 0.875em; -} -.tsd-sources a { - color: var(--color-text-aside); - text-decoration: underline; -} -.tsd-sources ul { - list-style: none; - padding: 0; -} - -.tsd-page-toolbar { - position: sticky; - z-index: 1; - top: 0; - left: 0; - width: 100%; - color: var(--color-text); - background: var(--color-background-secondary); - border-bottom: 1px var(--color-accent) solid; - transition: transform 0.3s ease-in-out; -} -.tsd-page-toolbar a { - color: var(--color-text); - text-decoration: none; -} -.tsd-page-toolbar a.title { - font-weight: bold; -} -.tsd-page-toolbar a.title:hover { - text-decoration: underline; -} -.tsd-page-toolbar .tsd-toolbar-contents { - display: flex; - justify-content: space-between; - height: 2.5rem; - margin: 0 auto; -} -.tsd-page-toolbar .table-cell { - position: relative; - white-space: nowrap; - line-height: 40px; -} -.tsd-page-toolbar .table-cell:first-child { - width: 100%; -} -.tsd-page-toolbar .tsd-toolbar-icon { - box-sizing: border-box; - line-height: 0; - padding: 12px 0; -} - -.tsd-widget { - display: inline-block; - overflow: hidden; - opacity: 0.8; - height: 40px; - transition: - opacity 0.1s, - background-color 0.2s; - vertical-align: bottom; - cursor: pointer; -} -.tsd-widget:hover { - opacity: 0.9; -} -.tsd-widget.active { - opacity: 1; - background-color: var(--color-accent); -} -.tsd-widget.no-caption { - width: 40px; -} -.tsd-widget.no-caption:before { - margin: 0; -} + a.tsd-index-link { + padding: 0.25rem 0 !important; + font-size: 1rem; + line-height: 1.25rem; + display: inline-flex; + align-items: center; + color: var(--color-text); + } + .tsd-accordion-summary { + list-style-type: none; /* hide marker on non-safari */ + outline: none; /* broken on safari, so just hide it */ + } + .tsd-accordion-summary::-webkit-details-marker { + display: none; /* hide marker on safari */ + } + .tsd-accordion-summary, + .tsd-accordion-summary a { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + + cursor: pointer; + } + .tsd-accordion-summary a { + width: calc(100% - 1.5rem); + } + .tsd-accordion-summary > * { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + } + .tsd-accordion .tsd-accordion-summary > svg { + margin-left: 0.25rem; + vertical-align: text-top; + } + /* + We need to be careful to target the arrow indicating whether the accordion + is open, but not any other SVGs included in the details element. +*/ + .tsd-accordion:not([open]) > .tsd-accordion-summary > svg:first-child, + .tsd-accordion:not([open]) > .tsd-accordion-summary > h1 > svg:first-child, + .tsd-accordion:not([open]) > .tsd-accordion-summary > h2 > svg:first-child, + .tsd-accordion:not([open]) > .tsd-accordion-summary > h3 > svg:first-child, + .tsd-accordion:not([open]) > .tsd-accordion-summary > h4 > svg:first-child { + transform: rotate(-90deg); + } + .tsd-index-content > :not(:first-child) { + margin-top: 0.75rem; + } + .tsd-index-heading { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + } -.tsd-widget.options, -.tsd-widget.menu { - display: none; -} -input[type="checkbox"] + .tsd-widget:before { - background-position: -120px 0; -} -input[type="checkbox"]:checked + .tsd-widget:before { - background-position: -160px 0; -} + .tsd-no-select { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + .tsd-kind-icon { + margin-right: 0.5rem; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + } + .tsd-signature > .tsd-kind-icon { + margin-right: 0.8rem; + } -img { - max-width: 100%; -} + .tsd-panel { + margin-bottom: 2.5rem; + } + .tsd-panel.tsd-member { + margin-bottom: 4rem; + } + .tsd-panel:empty { + display: none; + } + .tsd-panel > h1, + .tsd-panel > h2, + .tsd-panel > h3 { + margin: 1.5rem -1.5rem 0.75rem -1.5rem; + padding: 0 1.5rem 0.75rem 1.5rem; + } + .tsd-panel > h1.tsd-before-signature, + .tsd-panel > h2.tsd-before-signature, + .tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: none; + } -.tsd-anchor-icon { - display: inline-flex; - align-items: center; - margin-left: 0.5rem; - vertical-align: middle; - color: var(--color-text); -} + .tsd-panel-group { + margin: 2rem 0; + } + .tsd-panel-group.tsd-index-group { + margin: 2rem 0; + } + .tsd-panel-group.tsd-index-group details { + margin: 2rem 0; + } + .tsd-panel-group > .tsd-accordion-summary { + margin-bottom: 1rem; + } -.tsd-anchor-icon svg { - width: 1em; - height: 1em; - visibility: hidden; -} + #tsd-search { + transition: background-color 0.2s; + } + #tsd-search .title { + position: relative; + z-index: 2; + } + #tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 2.5rem; + height: 100%; + } + #tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); + } + #tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; + } + #tsd-search .field input, + #tsd-search .title, + #tsd-toolbar-links a { + transition: opacity 0.2s; + } + #tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + } + #tsd-search .results li { + background-color: var(--color-background); + line-height: initial; + padding: 4px; + } + #tsd-search .results li:nth-child(even) { + background-color: var(--color-background-secondary); + } + #tsd-search .results li.state { + display: none; + } + #tsd-search .results li.current:not(.no-results), + #tsd-search .results li:hover:not(.no-results) { + background-color: var(--color-accent); + } + #tsd-search .results a { + display: flex; + align-items: center; + padding: 0.25rem; + box-sizing: border-box; + } + #tsd-search .results a:before { + top: 10px; + } + #tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; + } + #tsd-search.has-focus { + background-color: var(--color-accent); + } + #tsd-search.has-focus .field input { + top: 0; + opacity: 1; + } + #tsd-search.has-focus .title, + #tsd-search.has-focus #tsd-toolbar-links a { + z-index: 0; + opacity: 0; + } + #tsd-search.has-focus .results { + visibility: visible; + } + #tsd-search.loading .results li.state.loading { + display: block; + } + #tsd-search.failure .results li.state.failure { + display: block; + } -.tsd-anchor-link:hover > .tsd-anchor-icon svg { - visibility: visible; -} + #tsd-toolbar-links { + position: absolute; + top: 0; + right: 2rem; + height: 100%; + display: flex; + align-items: center; + justify-content: flex-end; + } + #tsd-toolbar-links a { + margin-left: 1.5rem; + } + #tsd-toolbar-links a:hover { + text-decoration: underline; + } -.deprecated { - text-decoration: line-through !important; -} + .tsd-signature { + margin: 0 0 1rem 0; + padding: 1rem 0.5rem; + border: 1px solid var(--color-accent); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; + } -.warning { - padding: 1rem; - color: var(--color-warning-text); - background: var(--color-background-warning); -} + .tsd-signature-keyword { + color: var(--color-ts-keyword); + font-weight: normal; + } -.tsd-kind-project { - color: var(--color-ts-project); -} -.tsd-kind-module { - color: var(--color-ts-module); -} -.tsd-kind-namespace { - color: var(--color-ts-namespace); -} -.tsd-kind-enum { - color: var(--color-ts-enum); -} -.tsd-kind-enum-member { - color: var(--color-ts-enum-member); -} -.tsd-kind-variable { - color: var(--color-ts-variable); -} -.tsd-kind-function { - color: var(--color-ts-function); -} -.tsd-kind-class { - color: var(--color-ts-class); -} -.tsd-kind-interface { - color: var(--color-ts-interface); -} -.tsd-kind-constructor { - color: var(--color-ts-constructor); -} -.tsd-kind-property { - color: var(--color-ts-property); -} -.tsd-kind-method { - color: var(--color-ts-method); -} -.tsd-kind-call-signature { - color: var(--color-ts-call-signature); -} -.tsd-kind-index-signature { - color: var(--color-ts-index-signature); -} -.tsd-kind-constructor-signature { - color: var(--color-ts-constructor-signature); -} -.tsd-kind-parameter { - color: var(--color-ts-parameter); -} -.tsd-kind-type-literal { - color: var(--color-ts-type-literal); -} -.tsd-kind-type-parameter { - color: var(--color-ts-type-parameter); -} -.tsd-kind-accessor { - color: var(--color-ts-accessor); -} -.tsd-kind-get-signature { - color: var(--color-ts-get-signature); -} -.tsd-kind-set-signature { - color: var(--color-ts-set-signature); -} -.tsd-kind-type-alias { - color: var(--color-ts-type-alias); -} + .tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; + } -/* if we have a kind icon, don't color the text by kind */ -.tsd-kind-icon ~ span { - color: var(--color-text); -} + .tsd-signature-type { + font-style: italic; + font-weight: normal; + } -* { - scrollbar-width: thin; - scrollbar-color: var(--color-accent) var(--color-icon-background); -} + .tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + list-style-type: none; + } + .tsd-signatures .tsd-signature { + margin: 0; + border-color: var(--color-accent); + border-width: 1px 0; + transition: background-color 0.1s; + } + .tsd-signatures .tsd-index-signature:not(:last-child) { + margin-bottom: 1em; + } + .tsd-signatures .tsd-index-signature .tsd-signature { + border-width: 1px; + } + .tsd-description .tsd-signatures .tsd-signature { + border-width: 1px; + } -*::-webkit-scrollbar { - width: 0.75rem; -} + ul.tsd-parameter-list, + ul.tsd-type-parameter-list { + list-style: square; + margin: 0; + padding-left: 20px; + } + ul.tsd-parameter-list > li.tsd-parameter-signature, + ul.tsd-type-parameter-list > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; + } + ul.tsd-parameter-list h5, + ul.tsd-type-parameter-list h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; + } + .tsd-sources { + margin-top: 1rem; + font-size: 0.875em; + } + .tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; + } + .tsd-sources ul { + list-style: none; + padding: 0; + } -*::-webkit-scrollbar-track { - background: var(--color-icon-background); -} + .tsd-page-toolbar { + position: sticky; + z-index: 1; + top: 0; + left: 0; + width: 100%; + color: var(--color-text); + background: var(--color-background-secondary); + border-bottom: 1px var(--color-accent) solid; + transition: transform 0.3s ease-in-out; + } + .tsd-page-toolbar a { + color: var(--color-text); + text-decoration: none; + } + .tsd-page-toolbar a.title { + font-weight: bold; + } + .tsd-page-toolbar a.title:hover { + text-decoration: underline; + } + .tsd-page-toolbar .tsd-toolbar-contents { + display: flex; + justify-content: space-between; + height: 2.5rem; + margin: 0 auto; + } + .tsd-page-toolbar .table-cell { + position: relative; + white-space: nowrap; + line-height: 40px; + } + .tsd-page-toolbar .table-cell:first-child { + width: 100%; + } + .tsd-page-toolbar .tsd-toolbar-icon { + box-sizing: border-box; + line-height: 0; + padding: 12px 0; + } -*::-webkit-scrollbar-thumb { - background-color: var(--color-accent); - border-radius: 999rem; - border: 0.25rem solid var(--color-icon-background); -} + .tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.8; + height: 40px; + transition: + opacity 0.1s, + background-color 0.2s; + vertical-align: bottom; + cursor: pointer; + } + .tsd-widget:hover { + opacity: 0.9; + } + .tsd-widget.active { + opacity: 1; + background-color: var(--color-accent); + } + .tsd-widget.no-caption { + width: 40px; + } + .tsd-widget.no-caption:before { + margin: 0; + } -/* mobile */ -@media (max-width: 769px) { .tsd-widget.options, .tsd-widget.menu { - display: inline-block; + display: none; } - - .container-main { - display: flex; + input[type="checkbox"] + .tsd-widget:before { + background-position: -120px 0; } - html .col-content { - float: none; + input[type="checkbox"]:checked + .tsd-widget:before { + background-position: -160px 0; + } + + img { max-width: 100%; - width: 100%; } - html .col-sidebar { - position: fixed !important; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - z-index: 1024; - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - padding: 1.5rem 1.5rem 0 0; - width: 75vw; - visibility: hidden; - background-color: var(--color-background); - transform: translate(100%, 0); + + .tsd-member-summary-name { + display: inline-flex; + align-items: center; + padding: 0.25rem; + text-decoration: none; } - html .col-sidebar > *:last-child { - padding-bottom: 20px; + + .tsd-anchor-icon { + display: inline-flex; + align-items: center; + margin-left: 0.5rem; + color: var(--color-text); } - html .overlay { - content: ""; - display: block; - position: fixed; - z-index: 1023; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.75); + + .tsd-anchor-icon svg { + width: 1em; + height: 1em; visibility: hidden; } - .to-has-menu .overlay { - animation: fade-in 0.4s; + .tsd-member-summary-name:hover > .tsd-anchor-icon svg, + .tsd-anchor-link:hover > .tsd-anchor-icon svg { + visibility: visible; } - .to-has-menu .col-sidebar { - animation: pop-in-from-right 0.4s; + .deprecated { + text-decoration: line-through !important; } - .from-has-menu .overlay { - animation: fade-out 0.4s; + .warning { + padding: 1rem; + color: var(--color-warning-text); + background: var(--color-background-warning); } - .from-has-menu .col-sidebar { - animation: pop-out-to-right 0.4s; + .tsd-kind-project { + color: var(--color-ts-project); } - - .has-menu body { - overflow: hidden; + .tsd-kind-module { + color: var(--color-ts-module); } - .has-menu .overlay { - visibility: visible; + .tsd-kind-namespace { + color: var(--color-ts-namespace); } - .has-menu .col-sidebar { - visibility: visible; - transform: translate(0, 0); - display: flex; - flex-direction: column; - gap: 1.5rem; - max-height: 100vh; - padding: 1rem 2rem; + .tsd-kind-enum { + color: var(--color-ts-enum); } - .has-menu .tsd-navigation { - max-height: 100%; + .tsd-kind-enum-member { + color: var(--color-ts-enum-member); } - #tsd-toolbar-links { - display: none; + .tsd-kind-variable { + color: var(--color-ts-variable); } - .tsd-navigation .tsd-nav-link { - display: flex; + .tsd-kind-function { + color: var(--color-ts-function); } -} - -/* one sidebar */ -@media (min-width: 770px) { - .container-main { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); - grid-template-areas: "sidebar content"; - margin: 2rem auto; + .tsd-kind-class { + color: var(--color-ts-class); } - - .col-sidebar { - grid-area: sidebar; + .tsd-kind-interface { + color: var(--color-ts-interface); } - .col-content { - grid-area: content; - padding: 0 1rem; + .tsd-kind-constructor { + color: var(--color-ts-constructor); } -} -@media (min-width: 770px) and (max-width: 1399px) { - .col-sidebar { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; - padding-top: 1rem; + .tsd-kind-property { + color: var(--color-ts-property); } - .site-menu { - margin-top: 1rem; + .tsd-kind-method { + color: var(--color-ts-method); + } + .tsd-kind-reference { + color: var(--color-ts-reference); + } + .tsd-kind-call-signature { + color: var(--color-ts-call-signature); + } + .tsd-kind-index-signature { + color: var(--color-ts-index-signature); + } + .tsd-kind-constructor-signature { + color: var(--color-ts-constructor-signature); + } + .tsd-kind-parameter { + color: var(--color-ts-parameter); + } + .tsd-kind-type-parameter { + color: var(--color-ts-type-parameter); + } + .tsd-kind-accessor { + color: var(--color-ts-accessor); + } + .tsd-kind-get-signature { + color: var(--color-ts-get-signature); + } + .tsd-kind-set-signature { + color: var(--color-ts-set-signature); + } + .tsd-kind-type-alias { + color: var(--color-ts-type-alias); } -} -/* two sidebars */ -@media (min-width: 1200px) { - .container-main { - grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax(0, 20rem); - grid-template-areas: "sidebar content toc"; + /* if we have a kind icon, don't color the text by kind */ + .tsd-kind-icon ~ span { + color: var(--color-text); } - .col-sidebar { - display: contents; + * { + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-icon-background); } - .page-menu { - grid-area: toc; - padding-left: 1rem; + *::-webkit-scrollbar { + width: 0.75rem; } - .site-menu { - grid-area: sidebar; + + *::-webkit-scrollbar-track { + background: var(--color-icon-background); } - .site-menu { - margin-top: 1rem 0; + *::-webkit-scrollbar-thumb { + background-color: var(--color-accent); + border-radius: 999rem; + border: 0.25rem solid var(--color-icon-background); } - .page-menu, - .site-menu { - max-height: calc(100vh - 2rem - 42px); - overflow: auto; - position: sticky; - top: 42px; + /* mobile */ + @media (max-width: 769px) { + .tsd-widget.options, + .tsd-widget.menu { + display: inline-block; + } + + .container-main { + display: flex; + } + html .col-content { + float: none; + max-width: 100%; + width: 100%; + } + html .col-sidebar { + position: fixed !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + padding: 1.5rem 1.5rem 0 0; + width: 75vw; + visibility: hidden; + background-color: var(--color-background); + transform: translate(100%, 0); + } + html .col-sidebar > *:last-child { + padding-bottom: 20px; + } + html .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + + .to-has-menu .overlay { + animation: fade-in 0.4s; + } + + .to-has-menu .col-sidebar { + animation: pop-in-from-right 0.4s; + } + + .from-has-menu .overlay { + animation: fade-out 0.4s; + } + + .from-has-menu .col-sidebar { + animation: pop-out-to-right 0.4s; + } + + .has-menu body { + overflow: hidden; + } + .has-menu .overlay { + visibility: visible; + } + .has-menu .col-sidebar { + visibility: visible; + transform: translate(0, 0); + display: flex; + flex-direction: column; + gap: 1.5rem; + max-height: 100vh; + padding: 1rem 2rem; + } + .has-menu .tsd-navigation { + max-height: 100%; + } + #tsd-toolbar-links { + display: none; + } + .tsd-navigation .tsd-nav-link { + display: flex; + } + } + + /* one sidebar */ + @media (min-width: 770px) { + .container-main { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + grid-template-areas: "sidebar content"; + margin: 2rem auto; + } + + .col-sidebar { + grid-area: sidebar; + } + .col-content { + grid-area: content; + padding: 0 1rem; + } + } + @media (min-width: 770px) and (max-width: 1399px) { + .col-sidebar { + max-height: calc(100vh - 2rem - 42px); + overflow: auto; + position: sticky; + top: 42px; + padding-top: 1rem; + } + .site-menu { + margin-top: 1rem; + } + } + + /* two sidebars */ + @media (min-width: 1200px) { + .container-main { + grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax( + 0, + 20rem + ); + grid-template-areas: "sidebar content toc"; + } + + .col-sidebar { + display: contents; + } + + .page-menu { + grid-area: toc; + padding-left: 1rem; + } + .site-menu { + grid-area: sidebar; + } + + .site-menu { + margin-top: 1rem; + } + + .page-menu, + .site-menu { + max-height: calc(100vh - 2rem - 42px); + overflow: auto; + position: sticky; + top: 42px; + } } } diff --git a/docs/classes/_mdf.js_amqp-provider.Receiver.Port.html b/docs/classes/_mdf.js_amqp-provider.Receiver.Port.html new file mode 100644 index 00000000..7b4e8cd1 --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider.Receiver.Port.html @@ -0,0 +1,16 @@ +Port | @mdf.js

Implementation of an AMQP Receiver port instance

+

Hierarchy (View Summary)

Constructors

Accessors

Methods

Constructors

Accessors

  • get client(): Client
  • Return the underlying port instance

    +

    Returns Client

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Close the port instance

    +

    Returns Promise<void>

  • Initialize the port instance

    +

    Returns Promise<void>

  • Stop the port instance

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_amqp-provider.Sender.Port.html b/docs/classes/_mdf.js_amqp-provider.Sender.Port.html new file mode 100644 index 00000000..1d37c523 --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider.Sender.Port.html @@ -0,0 +1,16 @@ +Port | @mdf.js

Implementation of an AMQP Sender port instance

+

Hierarchy (View Summary)

Constructors

Accessors

Methods

Constructors

Accessors

  • get client(): Client
  • Return the underlying port instance

    +

    Returns Client

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Close the port instance

    +

    Returns Promise<void>

  • Initialize the port instance

    +

    Returns Promise<void>

  • Stop the port instance

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_amqp-provider._internal_.BasePort.html b/docs/classes/_mdf.js_amqp-provider._internal_.BasePort.html new file mode 100644 index 00000000..efd23643 --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider._internal_.BasePort.html @@ -0,0 +1,60 @@ +BasePort | @mdf.js

This is the class that should be extended to implement a new specific Port.

+

This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

+
    +
  • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
  • +
  • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
  • +
  • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
  • +
  • Store the configuration PortConfig previously validated by the Manager.
  • +
+

What the user of this class should develop in the specific port:

+
    +
  • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
  • +
  • The Port.stop method, which is responsible stop services or disconnect from the +resources.
  • +
  • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
  • +
  • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
  • +
  • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
  • +
+

class diagram

+

In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

+
    +
  • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
  • +
  • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
  • +
  • unhealthy: should be emitted when the port has limited access to the resources.
  • +
  • healthy: should be emitted when the port has recovered the access to the resources.
  • +
+

class diagram

+

Check some examples of implementation in:

+ +

Type Parameters

  • Client

    Underlying client type, this is, the real client of the wrapped provider

    +

Hierarchy (View Summary)

Constructors

Accessors

Methods

Constructors

Accessors

  • get client(): Client
  • Return the underlying port instance

    +

    Returns Client

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Close the port instance

    +

    Returns Promise<void>

  • Initialize the port instance

    +

    Returns Promise<void>

  • Stop the port instance

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_amqp-provider._internal_.Container.html b/docs/classes/_mdf.js_amqp-provider._internal_.Container.html new file mode 100644 index 00000000..72daf9de --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider._internal_.Container.html @@ -0,0 +1,4 @@ +Container | @mdf.js

Hierarchy (View Summary)

Constructors

Constructors

  • Creates an instance of AMQP Container

    +

    Parameters

    • options: ConnectionOptions

      Connection options

      +

    Returns Container

diff --git a/docs/classes/_mdf.js_amqp-provider._internal_.Receiver.html b/docs/classes/_mdf.js_amqp-provider._internal_.Receiver.html new file mode 100644 index 00000000..283ba4d1 --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider._internal_.Receiver.html @@ -0,0 +1,12 @@ +Receiver | @mdf.js

Hierarchy (View Summary)

Constructors

Accessors

Methods

Constructors

  • Creates an instance of AMQP Container

    +

    Parameters

    • options: ConnectionOptions

      Connection options

      +

    Returns Receiver

Accessors

  • get client(): Receiver
  • Return the underlying AMQP receiver

    +

    Returns Receiver

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Perform the connection to the AMQP broker and the creation of the receiver

    +

    Returns Promise<void>

  • Perform the disconnection from the AMQP broker

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_amqp-provider._internal_.Sender.html b/docs/classes/_mdf.js_amqp-provider._internal_.Sender.html new file mode 100644 index 00000000..04a8b0d8 --- /dev/null +++ b/docs/classes/_mdf.js_amqp-provider._internal_.Sender.html @@ -0,0 +1,12 @@ +Sender | @mdf.js

Hierarchy (View Summary)

Constructors

Accessors

Methods

Constructors

  • Creates an instance of AMQP Sender

    +

    Parameters

    • options: ConnectionOptions

      Connection options

      +

    Returns Sender

Accessors

  • get client(): AwaitableSender
  • Return the underlying AMQP sender

    +

    Returns AwaitableSender

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Perform the connection to the AMQP broker and the creation of the sender

    +

    Returns Promise<void>

  • Perform the disconnection from the AMQP broker

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_core.Jobs.JobHandler.html b/docs/classes/_mdf.js_core.Jobs.JobHandler.html new file mode 100644 index 00000000..57fa4be7 --- /dev/null +++ b/docs/classes/_mdf.js_core.Jobs.JobHandler.html @@ -0,0 +1,83 @@ +JobHandler | @mdf.js

Class JobHandler<Type, Data, CustomHeaders, CustomOptions>

JobHandler class

+

Type Parameters

  • Type extends string

    Job type, used as selector for strategies in job processors

    +
  • Data = unknown

    Job payload

    +
  • CustomHeaders extends Record<string, any> = AnyHeaders

    Custom headers, used to pass specific information for job processors

    +
  • CustomOptions extends Record<string, any> = AnyOptions

    Custom options, used to pass specific information for job processors

    +

Hierarchy

  • EventEmitter
    • JobHandler

Implements

Constructors

Properties

createdAt: Date

Date object with the timestamp when the job was created

+
jobUserId: string

User job request identifier, defined by the user

+
jobUserUUID: string

Unique user job request identification, based on jobUserId

+

Job meta information, used to pass specific information for job processors

+
type: Type

Job type, used as selector for strategies in job processors

+
uuid: string

Unique job processing identification

+

Accessors

  • get errors(): undefined | Multi
  • Errors raised during the job

    +

    Returns undefined | Multi

  • get hasErrors(): boolean
  • True if the job task raised any error

    +

    Returns boolean

  • get processTime(): number
  • Return the process time in msec

    +

    Returns number

Methods

  • Add a new error in the job

    +

    Parameters

    • error: Multi | Crash

      error to be added to the job

      +

    Returns void

  • Register an event listener over the done event, which is emitted when a job has ended, either +due to completion or failure.

    +

    Parameters

    Returns this

  • Notify the results of the process

    +

    Parameters

    • Optionalerror: Crash

      conditional parameter for error notification

      +

    Returns void

  • Removes the specified listener from the listener array for the done event.

    +

    Parameters

    Returns this

  • Register an event listener over the done event, which is emitted when a job has ended, either +due to completion or failure.

    +

    Parameters

    Returns this

  • Registers a one-time event listener over the done event, which is emitted when a job has +ended, either due to completion or failure.

    +

    Parameters

    Returns this

  • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a job has ended, either due to completion or failure.

    +

    Parameters

    Returns this

  • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a job has ended, either due to completion or failure.

    +

    Parameters

    Returns this

  • Removes all listeners, or those of the specified event.

    +

    Parameters

    • Optionalevent: "done"

      done event

      +

    Returns this

  • Removes the specified listener from the listener array for the done event.

    +

    Parameters

    Returns this

diff --git a/docs/classes/_mdf.js_core.Layer.Provider.Manager.html b/docs/classes/_mdf.js_core.Layer.Provider.Manager.html new file mode 100644 index 00000000..5e51d468 --- /dev/null +++ b/docs/classes/_mdf.js_core.Layer.Provider.Manager.html @@ -0,0 +1,96 @@ +Manager | @mdf.js

Class Manager<PortClient, PortConfig, PortInstance>

Provider Manager wraps a specific port created by the extension of the Port abstract +class, instrumenting it with the necessary logic to manage:

+ +

class diagram

+
    +
  • Merge and validate the configuration of the provider represented by the generic type +PortConfig. The manager configuration object ProviderOptions has a validation +property that represent a structure of type PortConfigValidationStruct where default +values, environment based and a Joi validation object are +defined. During the initialization process, the manager will merge all the sources of +configuration (default, environment and specific) and validate the result against the Joi schema. +So, the order of priority of the configuration sources is: specific, environment and default. +If the validation fails, the manager will use the default values and emit an error that will be +managed by the observability layer.
  • +
+

Port class, this is, the class that extends the Port abstract class

+

Type Parameters

  • PortClient

    Underlying client type, this is, the real client of the wrapped provider

    +
  • PortConfig

    Port configuration object, could be an extended version of the client config

    +
  • PortInstance extends Layer.Provider.Port<PortClient, PortConfig>

Hierarchy

  • EventEmitter
    • Manager

Implements

Constructors

Properties

componentId: string

Provider unique identifier for trace purposes

+
config: PortConfig

Port configuration

+

Accessors

  • get date(): string
  • Timestamp of actual state in ISO format, when the current state was reached

    +

    Returns string

  • get error(): undefined | Multi | Crash
  • Return the errors in the provider

    +

    Returns undefined | Multi | Crash

  • get name(): string
  • Provider name

    +

    Returns string

  • get state(): "error" | "running" | "stopped"
  • Provider state

    +

    Returns "error" | "running" | "stopped"

  • get status(): "pass" | "fail" | "warn"
  • Provider status

    +

    Returns "pass" | "fail" | "warn"

Methods

  • Add a listener for the error event, emitted when the component detects an error.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Add a listener for the status event, emitted when the component changes its status.

    +

    Parameters

    • event: "status"

      status event

      +
    • listener: (status: ProviderStatus) => void

      Status event listener

      +

    Returns this

  • Close the provider: release resources, connections ...

    +

    Returns Promise<void>

  • Error state: wait for new state of to fix the actual degraded stated

    +

    Parameters

    • error: Error | Crash

      Cause ot this fail transition

      +

    Returns Promise<void>

  • Removes the specified listener from the listener array for the error event.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Removes the specified listener from the listener array for the status event.

    +

    Parameters

    • event: "status"

      status event

      +
    • listener: (status: ProviderStatus) => void

      Status event listener

      +

    Returns this

  • Add a listener for the error event, emitted when the component detects an error.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Add a listener for the status event, emitted when the component changes its status.

    +

    Parameters

    • event: "status"

      status event

      +
    • listener: (status: ProviderStatus) => void

      Status event listener

      +

    Returns this

  • Add a listener for the error event, emitted when the component detects an error. This is a +one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Add a listener for the status event, emitted when the component changes its status. This is a +one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "status"

      status event

      +
    • listener: (status: ProviderStatus) => void

      Status event listener

      +

    Returns this

  • Removes all listeners, or those of the specified event.

    +

    Parameters

    • Optionalevent: "error"

      error event

      +

    Returns this

  • Removes the specified listener from the listener array for the error event.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Removes the specified listener from the listener array for the status event.

    +

    Parameters

    • event: "status"

      status event

      +
    • listener: (status: ProviderStatus) => void

      Status event listener

      +

    Returns this

  • Initialize the process: internal jobs, external dependencies connections ...

    +

    Returns Promise<void>

  • Stop the process: internal jobs, external dependencies connections ...

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_core.Layer.Provider.Port.html b/docs/classes/_mdf.js_core.Layer.Provider.Port.html new file mode 100644 index 00000000..29586da9 --- /dev/null +++ b/docs/classes/_mdf.js_core.Layer.Provider.Port.html @@ -0,0 +1,131 @@ +Port | @mdf.js

Class Port<PortClient, PortConfig>Abstract

This is the class that should be extended to implement a new specific Port.

+

This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

+
    +
  • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
  • +
  • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
  • +
  • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
  • +
  • Store the configuration PortConfig previously validated by the Manager.
  • +
+

What the user of this class should develop in the specific port:

+
    +
  • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
  • +
  • The Port.stop method, which is responsible stop services or disconnect from the +resources.
  • +
  • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
  • +
  • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
  • +
  • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
  • +
+

class diagram

+

In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

+
    +
  • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
  • +
  • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
  • +
  • unhealthy: should be emitted when the port has limited access to the resources.
  • +
  • healthy: should be emitted when the port has recovered the access to the resources.
  • +
+

class diagram

+

Check some examples of implementation in:

+ +

Type Parameters

  • PortClient

    Underlying client type, this is, the real client of the wrapped provider

    +
  • PortConfig

    Port configuration object, could be an extended version of the client config

    +

Hierarchy (View Summary)

Constructors

Properties

Accessors

Methods

Constructors

Properties

config: PortConfig

Port configuration options

+
name: string

Port name, to be used as identifier

+
uuid: string = ...

Port unique identifier for trace purposes

+

Accessors

  • get checks(): Record<string, Check<any>[]>
  • Return the actual checks

    +

    Returns Record<string, Check<any>[]>

  • get state(): boolean
  • Return the port state as a boolean value, true if the port is available, false in otherwise

    +

    Returns boolean

Methods

  • Update or add a check measure. +This should be used to inform about the state of resources behind the Port, for example memory +usage, CPU usage, etc.

    +

    The new check will be taking into account in the overall health status. +The new check will be included in the checks object with the key indicated in the param +measure.* If this key already exists, the componentId of the check parameter will be +checked, if there is a check with the same componentId in the array, the check will be +updated, in other case the new check will be added to the existing array.

    +

    The maximum number external checks is 100

    +

    Parameters

    • measure: string

      measure identification

      +
    • check: Check<any>

      check to be updated or included

      +

    Returns boolean

    true, if the check has been updated

    +
  • Close the port, making it no longer available

    +

    Returns Promise<void>

  • Emit an error event, to notify errors in the resource management or access, this will change +the provider state by the upper manager.

    +

    Parameters

    • event: "error"

      error event

      +
    • error: Multi | Crash

      Error to be notified to the upper manager

      +

    Returns boolean

  • Emit a closed event, to notify that the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used. This +event will change the provider state by the upper manager.

    +

    Parameters

    • event: "closed"

      closed event

      +
    • Optionalerror: Multi | Crash

      Error to be notified to the upper manager, if any

      +

    Returns boolean

  • Emit an unhealthy event, to notify that the port has limited access to the resources. This +event will change the provider state by the upper manager.

    +

    Parameters

    • event: "unhealthy"

      unhealthy event

      +
    • error: Multi | Crash

      Error to be notified to the upper manager

      +

    Returns boolean

  • Emit a healthy event, to notify that the port has recovered the access to the resources. This +event will change the provider state by the upper manager.

    +

    Parameters

    • event: "healthy"

      healthy event

      +

    Returns boolean

  • Add a listener for the error event, emitted when the component detects an error.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Add a listener for the closed event, emitted when the port resources are no longer available

    +

    Parameters

    • event: "closed"

      closed event

      +
    • listener: (error?: Multi | Crash) => void

      Closed event listener

      +

    Returns this

  • Add a listener for the unhealthy event, emitted when the port has limited access to the +resources

    +

    Parameters

    • event: "unhealthy"

      unhealthy event

      +
    • listener: (error: Multi | Crash) => void

      Unhealthy event listener

      +

    Returns this

  • Add a listener for the healthy event, emitted when the port has recovered the access to the +resources

    +

    Parameters

    • event: "healthy"

      healthy event

      +
    • listener: () => void

      Healthy event listener

      +

    Returns this

  • Add a listener for the error event, emitted when the component detects an error. This is a +one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "error"

      error event

      +
    • listener: (error: Multi | Crash) => void

      Error event listener

      +

    Returns this

  • Add a listener for the closed event, emitted when the port resources are no longer available. +This is a one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "closed"

      closed event

      +
    • listener: (error?: Multi | Crash) => void

      Closed event listener

      +

    Returns this

  • Add a listener for the unhealthy event, emitted when the port has limited access to the +resources. This is a one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "unhealthy"

      unhealthy event

      +
    • listener: (error: Multi | Crash) => void

      Unhealthy event listener

      +

    Returns this

  • Add a listener for the healthy event, emitted when the port has recovered the access to the +resources. This is a one-time event, the listener will be removed after the first emission.

    +

    Parameters

    • event: "healthy"

      healthy event

      +
    • listener: () => void

      Healthy event listener

      +

    Returns this

  • Start the port, making it available

    +

    Returns Promise<void>

  • Stop the port, making it unavailable

    +

    Returns Promise<void>

diff --git a/docs/classes/_mdf.js_crash.Boom.html b/docs/classes/_mdf.js_crash.Boom.html new file mode 100644 index 00000000..ad1324d5 --- /dev/null +++ b/docs/classes/_mdf.js_crash.Boom.html @@ -0,0 +1,62 @@ +Boom | @mdf.js

Improved error handling in REST-API interfaces

+

Boom helps us with error responses (HTTP Codes 3XX-5XX) within our REST-API interface by +providing us with some tools:

+
    +
  • Helpers for the rapid generation of standard responses.
  • +
  • Association of errors and their causes in a hierarchical way.
  • +
  • Adaptation of validation errors of the Joi library.
  • +
+

In addition, in combination with the Multi error types, errors in validation processes, and +Crash, standard application errors, it allows a complete management of the different types of +errors in our backend.

+

Hierarchy (View Summary)

Constructors

Properties

Accessors

Methods

Constructors

  • Create a new Boom error

    +

    Parameters

    • message: string

      human friendly error message

      +
    • uuid: string

      unique identifier for this particular occurrence of the problem

      +
    • code: number = 500

      HTTP Standard error code

      +
    • Optionaloptions: BoomOptions

      enhanced error options

      +

    Returns Boom

Properties

date: Date

Error date

+
name: string = 'BaseError'

Error name (type)

+
subject: string

Error subject, 'common' as default

+

Accessors

  • get cause(): undefined | Cause
  • Cause source of error

    +

    Returns undefined | Cause

  • get info(): undefined | Record<string, unknown>
  • Return the info object for this error

    +

    Returns undefined | Record<string, unknown>

  • get isBoom(): boolean
  • Boom error

    +

    Returns boolean

  • Links that leads to further details about this particular occurrence of the problem. +A link MUST be represented as either:

    +
      +
    • self: a string containing the link’s URL
    • +
    • related: an object (“link object”) which can contain the following members: +
        +
      • href: a string containing the link’s URL.
      • +
      • meta: a meta object containing non-standard meta-information about the link.
      • +
      +
    • +
    +

    Returns undefined | Links

  • get resource(): undefined | APISource
  • Object with the key information of the requested resource in the REST API context

    +

    Returns undefined | APISource

  • get source(): undefined | APISource
  • Object with the key information of the requested resource in the REST API context

    +

    Returns undefined | APISource

      +
    • source has been deprecated, use resource instead
    • +
    +
  • get status(): number
  • Boom error code

    +

    Returns number

  • get uuid(): string
  • Return the unique identifier associated to this instance

    +

    Returns string

Methods

  • Transform joi Validation error in a Boom error

    +

    Parameters

    Returns void

  • Return a string formatted as name:message

    +

    Returns string

  • Get the trace of this hierarchy of errors

    +

    Returns string[]

diff --git a/docs/classes/_mdf_js_crash.BoomHelpers.html b/docs/classes/_mdf.js_crash.BoomHelpers.html similarity index 52% rename from docs/classes/_mdf_js_crash.BoomHelpers.html rename to docs/classes/_mdf.js_crash.BoomHelpers.html index aaf2b959..3cff40c4 100644 --- a/docs/classes/_mdf_js_crash.BoomHelpers.html +++ b/docs/classes/_mdf.js_crash.BoomHelpers.html @@ -1,138 +1,138 @@ -BoomHelpers | @mdf.js

Helpers for easy generation of Boom kind errors

+BoomHelpers | @mdf.js

Helpers for easy generation of Boom kind errors

-

Methods

  • The HyperText Transfer Protocol (HTTP) 502 Bad Gateway server error response code indicates +

Methods

  • The HyperText Transfer Protocol (HTTP) 502 Bad Gateway server error response code indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 400 Bad Request response status code indicates that the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 400 Bad Request response status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). The client should not repeat this request without modification.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 409 Conflict response status code indicates a request conflict with current state of +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 409 Conflict response status code indicates a request conflict with current state of the server. Conflicts are most likely to occur in response to a PUT request. For example, you may get a 409 response when uploading a file which is older than the one already on the server resulting in a version control conflict.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 417 Expectation Failed client error response code indicates that the expectation given +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 417 Expectation Failed client error response code indicates that the expectation given in the request's Expect header could not be met. See the Expect header for more details.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The 424 (Failed Dependency) status code means that the method could not be performed on the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The 424 (Failed Dependency) status code means that the method could not be performed on the resource because the requested action depended on another action and that action failed. For example, if a command in a PROPPATCH method fails, then, at minimum, the rest of the commands will also fail with 424 (Failed Dependency).

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 403 Forbidden client error status response code indicates that the server understood +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 403 Forbidden client error status response code indicates that the server understood the request but refuses to authorize it. This status is similar to 401, but in this case, re-authenticating will make no difference. The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 504 Gateway Timeout server error response code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 504 Gateway Timeout server error response code indicates that the server, while acting as a gateway or proxy, did not get a response in time from the upstream server that it needed in order to complete the request.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 410 Gone client error response code indicates that +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 410 Gone client error response code indicates that access to the target resource is no longer available at the origin server and that this condition is likely to be permanent. If you don't know whether this condition is temporary or permanent, a 404 status code should be used instead.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 431 Request Header Fields Too Large response status code indicates that the server +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 431 Request Header Fields Too Large response status code indicates that the server refuses to process the request because the request’s HTTP headers are too long. The request may be resubmitted after reducing the size of the request headers. 431 can be used when the total size of request headers is too large, or when a single header @@ -146,14 +146,14 @@

Parameters

  • message: string

    Human-readable explanation specific to this occurrence of the problem

  • uuid: string

    UUID V4, unique identifier for this particular occurrence of the problem

    -
  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    -

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 451 Unavailable For Legal Reasons client error response +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 451 Unavailable For Legal Reasons client error response code indicates that the user requested a resource that is not available due to legal reasons, such as a web page for which a legal action has been issued.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 500 Internal Server Error server error response code +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 500 Internal Server Error server error response code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request. This error response is a generic "catch-all" response. Usually, this indicates the server @@ -162,26 +162,26 @@ from happening again in the future.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 411 Length Required client error response code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 411 Length Required client error response code indicates that the server refuses to accept the request without a defined Content-Length header.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The 423 (Locked) status code means the source or destination resource of a method is locked. +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The 423 (Locked) status code means the source or destination resource of a method is locked. This response SHOULD contain an appropriate precondition or postcondition code, such as 'lock-token-submitted' or 'no-conflicting-lock'.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed response status code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed response status code indicates that the request method is known by the server but is not supported by the target resource. The server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 406 Not Acceptable client error response code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 406 Not Acceptable client error response code indicates that the server cannot produce a response matching the list of acceptable values defined in the request's proactive content negotiation headers, and that the server is unwilling to supply a default representation. @@ -193,16 +193,16 @@ the available representations of the resources, allowing the user to choose among them.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 404 Not Found client error response code indicates that the server can't find the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 404 Not Found client error response code indicates that the server can't find the requested resource. Links which lead to a 404 page are often called broken or dead links, and can be subject to link rot. A 404 status code does not indicate whether the resource is temporarily or permanently missing. But if a resource is permanently removed, a 410 (Gone) should be used instead of a 404 status.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 501 Not Implemented server error response code means +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 501 Not Implemented server error response code means that the server does not support the functionality required to fulfill the request. This status can also send a Retry-After header, telling the requester when to check back to see if the functionality is supported by then. @@ -213,14 +213,14 @@ response is 405 Method Not Allowed.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 413 Payload Too Large response status code indicates that the request entity is larger +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 413 Payload Too Large response status code indicates that the request entity is larger than limits defined by server; the server might close the connection or return a Retry-After header field.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 402 Payment Required is a nonstandard client error status response code that is +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 402 Payment Required is a nonstandard client error status response code that is reserved for future use. Sometimes, this code indicates that the request can not be processed until the client makes a payment. Originally it was created to enable digital cash or (micro) payment systems and would @@ -228,8 +228,8 @@ no standard use convention exists and different entities use it in different contexts.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 412 Precondition Failed client error response code +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 412 Precondition Failed client error response code indicates that access to the target resource has been denied. This happens with conditional requests on methods other than GET or HEAD when the condition defined by the If-Unmodified-Since or If-None-Match headers is not fulfilled. In that case, the request, @@ -237,24 +237,24 @@ sent back.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 428 Precondition Required response status code indicates that the server requires the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 428 Precondition Required response status code indicates that the server requires the request to be conditional. Typically, this means that a required precondition header, such as If-Match, is missing. When a precondition header is not matching the server side state, the response should be 412 Precondition Failed.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 407 Proxy Authentication Required client error status response code indicates that the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 407 Proxy Authentication Required client error status response code indicates that the request has not been applied because it lacks valid authentication credentials for a proxy server that is between the browser and the server that can access the requested resource. This status is sent with a Proxy-Authenticate header that contains information on how to authorize correctly.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 416 Range Not Satisfiable error response code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 416 Range Not Satisfiable error response code indicates that a server cannot serve the requested ranges. The most likely reason is that the document doesn't contain such ranges, or that the Range header value, though syntactically correct, doesn't make sense. @@ -264,8 +264,8 @@ will be considered as non-resumable) or ask for the whole document again.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 408 Request Timeout response status code means that the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 408 Request Timeout response status code means that the server would like to shut down this unused connection. It is sent on an idle connection by some servers, even without any previous request by the client. A server should send the "close" Connection header field in the response, since 408 implies @@ -274,8 +274,8 @@ HTTP pre-connection mechanisms to speed up surfing.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code indicates that the server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, @@ -284,55 +284,55 @@ 503 status is often a temporary condition and responses shouldn't usually be cached.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 418 I'm a teapot client error response code indicates that the server refuses to brew +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 418 I'm a teapot client error response code indicates that the server refuses to brew coffee because it is, permanently, a teapot. A combined coffee/tea pot that is temporarily out of coffee should instead return 503. This error is a reference to Hyper Text Coffee Pot Control Protocol defined in April Fools' jokes in 1998 and 2014.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 425 Too Early response status code indicates that the +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 425 Too Early response status code indicates that the server is unwilling to risk processing a request that might be replayed, which creates the potential for a replay attack.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 429 Too Many Requests response status code indicates the user has sent too many +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 429 Too Many Requests response status code indicates the user has sent too many requests in a given amount of time ("rate limiting"). A Retry-After header might be included to this response indicating how long to wait before making a new request

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 401 Unauthorized client error status response code indicates that the request has not +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 401 Unauthorized client error status response code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. This status is sent with a WWW-Authenticate header that contains information on how to authorize correctly.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HyperText Transfer Protocol (HTTP) 422 Unprocessable Entity response status code indicates +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HyperText Transfer Protocol (HTTP) 422 Unprocessable Entity response status code indicates that the server understands the content type of the request entity, and the syntax of the request entity is correct, but it was unable to process the contained instructions.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 415 Unsupported Media Type client error response code indicates that the server +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 415 Unsupported Media Type client error response code indicates that the server refuses to accept the request because the payload format is in an unsupported format. The format problem might be due to the request's indicated Content-Type or Content-Encoding, or as a result of inspecting the data directly.

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 426 Upgrade Required client error response code indicates that the server refuses to +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 426 Upgrade Required client error response code indicates that the server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol. The server sends an Upgrade header with this response to indicate the required protocol(s).

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

  • The HTTP 414 URI Too Long response status code indicates that the URI requested by the client +

  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +

Returns Boom

  • The HTTP 414 URI Too Long response status code indicates that the URI requested by the client is longer than the server is willing to interpret. There are a few rare conditions when this might occur:

      @@ -345,5 +345,5 @@

    Parameters

    • message: string

      Human-readable explanation specific to this occurrence of the problem

    • uuid: string

      UUID V4, unique identifier for this particular occurrence of the problem

      -
    • Optionaloptions: BoomOptions

      Specific options for enhanced error management

      -

    Returns Boom

+
  • Optionaloptions: BoomOptions

    Specific options for enhanced error management

    +
  • Returns Boom

    diff --git a/docs/classes/_mdf.js_crash.Crash.html b/docs/classes/_mdf.js_crash.Crash.html new file mode 100644 index 00000000..db721ac0 --- /dev/null +++ b/docs/classes/_mdf.js_crash.Crash.html @@ -0,0 +1,61 @@ +Crash | @mdf.js

    Improved handling of standard errors.

    +

    Crash helps us manage standard errors within our application by providing us with some tools:

    +
      +
    • Association of errors and their causes in a hierarchical way.
    • +
    • Simple search for root causes within the hierarchy of errors.
    • +
    • Stack management, both of the current instance of the error, and of the causes.
    • +
    • Facilitate error logging.
    • +
    +

    In addition, in combination with the Multi error types, errors in validation processes, and Boom, +errors for the REST-API interfaces, it allows a complete management of the different types of +errors in our backend.

    +

    Hierarchy (View Summary)

    Constructors

    • Create a new Crash error instance

      +

      Parameters

      • message: string

        human friendly error message

        +

      Returns Crash

    • Create a new Crash error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • options: CrashOptions

        enhanced error options

        +

      Returns Crash

    • Create a new Crash error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • uuid: string

        unique identifier for this particular occurrence of the problem

        +

      Returns Crash

    • Create a new Crash error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • uuid: string

        unique identifier for this particular occurrence of the problem

        +
      • options: CrashOptions

        enhanced error options

        +

      Returns Crash

    Properties

    date: Date

    Error date

    +
    name: string = 'BaseError'

    Error name (type)

    +
    subject: string

    Error subject, 'common' as default

    +

    Accessors

    • get cause(): undefined | Cause
    • Cause source of error

      +

      Returns undefined | Cause

    • get info(): undefined | Record<string, unknown>
    • Return the info object for this error

      +

      Returns undefined | Record<string, unknown>

    • get isCrash(): boolean
    • Determine if this instance is a Crash error

      +

      Returns boolean

    • get uuid(): string
    • Return the unique identifier associated to this instance

      +

      Returns string

    Methods

    • Look in the nested causes of the error and return the first occurrence of a cause with the +indicated name

      +

      Parameters

      • name: string

        name of the error to search for

        +

      Returns undefined | Cause

      the cause, if there is any present with that name

      +
    • Returns a full stack of the error and causes hierarchically. The string contains the +description of the point in the code at which the Error/Crash was instantiated

      +

      Returns undefined | string

    • Check if there is any cause in the stack with the indicated name

      +

      Parameters

      • name: string

        name of the error to search for

        +

      Returns boolean

      Boolean value as the result of the search

      +
    • Return a string formatted as name:message

      +

      Returns string

    • Get the trace of this hierarchy of errors

      +

      Returns string[]

    • Check if an object is a valid Crash or Multi error

      +

      Parameters

      • error: unknown

        error to be checked

        +
      • Optionaluuid: string

        Optional uuid to be used instead of a random one.

        +

      Returns Crash | Multi

    diff --git a/docs/classes/_mdf.js_crash.Multi.html b/docs/classes/_mdf.js_crash.Multi.html new file mode 100644 index 00000000..f7af3d9e --- /dev/null +++ b/docs/classes/_mdf.js_crash.Multi.html @@ -0,0 +1,71 @@ +Multi | @mdf.js

    Improved handling of validation errors.

    +

    Multi helps us to manage validation or information transformation errors, in other words, it +helps us manage any process that may generate multiple non-hierarchical errors (an error is not a +direct consequence of the previous one) by providing us with some tools:

    +
      +
    • Management of the error stack.
    • +
    • Simple search for root causes within the error stack.
    • +
    • Stack management, both of the current instance of the error, and of the causes.
    • +
    • Facilitate error logging.
    • +
    +

    Furthermore, in combination with the types of error Boom, errors for the REST-API interfaces, and +Crash, standard application errors, it allows a complete management of the different types of +errors in our backend.

    +

    Hierarchy (View Summary)

    Constructors

    • Create a new Multi error

      +

      Parameters

      • message: string

        human friendly error message

        +

      Returns Multi

    • Create a new Multi error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • options: MultiOptions

        enhanced error options

        +

      Returns Multi

    • Create a new Multi error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • uuid: string

        unique identifier for this particular occurrence of the problem

        +

      Returns Multi

    • Create a new Multi error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • uuid: string

        unique identifier for this particular occurrence of the problem

        +
      • options: MultiOptions

        enhanced error options

        +

      Returns Multi

    Properties

    date: Date

    Error date

    +
    name: string = 'BaseError'

    Error name (type)

    +
    subject: string

    Error subject, 'common' as default

    +

    Accessors

    • get causes(): undefined | Cause[]
    • Causes source of error

      +

      Returns undefined | Cause[]

    • get info(): undefined | Record<string, unknown>
    • Return the info object for this error

      +

      Returns undefined | Record<string, unknown>

    • get isMulti(): boolean
    • Determine if this instance is a Multi error

      +

      Returns boolean

    • get size(): number
    • Return the number of causes of this error

      +

      Returns number

    • get uuid(): string
    • Return the unique identifier associated to this instance

      +

      Returns string

    Methods

    • Look in the nested causes of the error and return the first occurrence of a cause with the +indicated name

      +

      Parameters

      • name: string

        name of the error to search for

        +

      Returns undefined | Cause

      the cause, if there is any present with that name

      +
    • Returns a full stack of the error and causes hierarchically. The string contains the +description of the point in the code at which the Error/Crash/Multi was instantiated

      +

      Returns undefined | string

    • Check if there is any cause in the stack with the indicated name

      +

      Parameters

      • name: string

        name of the error to search for

        +

      Returns boolean

      Boolean value as the result of the search

      +
    • Process the errors thrown by Joi into the cause array

      +

      Parameters

      Returns number

      number or error that have been introduced

      +
    • Remove a error from the array of causes

      +

      Returns undefined | Cause

      the cause that have been removed

      +
    • Add a new error on the array of causes

      +

      Parameters

      • error: Cause

        Cause to be added to the array of causes

        +

      Returns void

    • Return a string formatted as name:message

      +

      Returns string

    • Get the trace of this hierarchy of errors

      +

      Returns string[]

    diff --git a/docs/classes/_mdf.js_crash._internal_.Base.html b/docs/classes/_mdf.js_crash._internal_.Base.html new file mode 100644 index 00000000..b3658e9c --- /dev/null +++ b/docs/classes/_mdf.js_crash._internal_.Base.html @@ -0,0 +1,19 @@ +Base | @mdf.js

    Class Base, manages errors in MDF Systems

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Create a new Base error

      +

      Parameters

      • message: string

        human friendly error message

        +
      • Optionaluuid: string | BaseOptions

        unique identifier for this particular occurrence of the problem

        +
      • Optionaloptions: BaseOptions

        enhanced error options

        +

      Returns Base

    Properties

    date: Date

    Error date

    +
    name: string = 'BaseError'

    Error name (type)

    +
    subject: string

    Error subject, 'common' as default

    +

    Accessors

    • get info(): undefined | Record<string, unknown>
    • Return the info object for this error

      +

      Returns undefined | Record<string, unknown>

    • get uuid(): string
    • Return the unique identifier associated to this instance

      +

      Returns string

    Methods

    • Return a string formatted as name:message

      +

      Returns string

    diff --git a/docs/classes/_mdf.js_doorkeeper.DoorKeeper.html b/docs/classes/_mdf.js_doorkeeper.DoorKeeper.html new file mode 100644 index 00000000..849071f8 --- /dev/null +++ b/docs/classes/_mdf.js_doorkeeper.DoorKeeper.html @@ -0,0 +1,78 @@ +DoorKeeper | @mdf.js

    Doorkeeper is a wrapper for AJV that allows us to validate JSONs against schemas. +It also allows us to register schemas and retrieve them later.

    +

    Type Parameters

    • T = void

    Constructors

    Properties

    Doorkeeper options

    +
    uuid: string = ...

    Methods

    • Try to validate an Object against the input schema or throw a ValidationError

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +

      Returns ValidatedOutput<T, K>

    • Try to validate an Object against the input schema or throw a ValidationError

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +
      • uuid: string

        unique identifier for this operation

        +

      Returns ValidatedOutput<T, K>

    • Validate an Object against the input schema and return a boolean

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +

      Returns boolean

    • Validate an Object against the input schema and return a boolean

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +
      • uuid: string

        unique identifier for this operation

        +

      Returns boolean

    • Experimental

      Return a dereferenced schema with all the $ref resolved

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to dereference

        +
      • uuid: string = ...

        unique identifier for this operation

        +

      Returns AnySchema

      A dereferenced schema with all the $ref resolved +This method is experimental and might change in the future without notice or be +removed from a future release. Use it at your own risk.

      +
    • Checks if the given data matches the specified schema.

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema to check against.

        +
      • data: any

        The data to validate.

        +

      Returns data is ValidatedOutput<T, K>

      A boolean indicating whether the data matches the schema.

      +
    • Checks if the input schema is registered

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        schema asked for

        +

      Returns boolean

        +
      • if the schema is registered in the ajv collection
      • +
      +
    • Registers a group of schemas from an object using the keys of the +object as key and the value as the validation schema

      +

      Parameters

      • schemas: Record<SchemaSelector<T>, AnySchema>

        Object containing the [key, validation schema]

        +

      Returns DoorKeeper<T>

        +
      • the instance
      • +
      +
    • Registers a group of schemas from an array and compiles them

      +

      Parameters

      • schemas: AnySchema[]

        Array containing the

        +

      Returns DoorKeeper<T>

        +
      • the instance
      • +
      +
    • Registers one schema with its key

      +

      Parameters

      • key: SchemaSelector<T>

        the key with which identify the schema

        +
      • validatorSchema: AnySchema

        the schema to be registered

        +

      Returns DoorKeeper<T>

        +
      • the instance
      • +
      +
    • Validate an Object against the input schema

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +
      • uuid: string

        unique identifier for this operation

        +
      • callback: ResultCallback<T, K>

        callback function with the result of the validation

        +

      Returns void

    • Validate an Object against the input schema

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +
      • callback: ResultCallback<T, K>

        callback function with the result of the validation

        +

      Returns void

    • Validate an Object against the input schema

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +
      • uuid: string

        unique identifier for this operation

        +

      Returns Promise<ValidatedOutput<T, K>>

    • Validate an Object against the input schema

      +

      Type Parameters

      • K extends string

      Parameters

      • schema: K

        The schema we want to validate

        +
      • data: any

        Object to be validated

        +

      Returns Promise<ValidatedOutput<T, K>>

    diff --git a/docs/classes/_mdf_js_elastic_provider.Elastic.Port.html b/docs/classes/_mdf.js_elastic-provider.Elastic.Port.html similarity index 52% rename from docs/classes/_mdf_js_elastic_provider.Elastic.Port.html rename to docs/classes/_mdf.js_elastic-provider.Elastic.Port.html index 7c127cf4..adca41af 100644 --- a/docs/classes/_mdf_js_elastic_provider.Elastic.Port.html +++ b/docs/classes/_mdf.js_elastic-provider.Elastic.Port.html @@ -1,11 +1,11 @@ -Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    This class implements some util logic to facilitate the creation of new Ports, for this reason is exposed as abstract class, instead of an interface. The basic operations that already implemented in the class are:

      -
    • Health.Checks management: using the Port.addCheck method is +
    • Health.Checks management: using the Port.addCheck method is possible to include new observed values that will be used in the observability layers.
    • -
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error traceability.
    • Establish the context for the logger to simplify the identification of the port in the logs, this is, it's not necessary to indicate the uuid and context in each logging function call.
    • @@ -13,46 +13,46 @@

    What the user of this class should develop in the specific port:

      -
    • The Port.start method, which is responsible initialize or stablish the connection to +
    • The Port.start method, which is responsible initialize or stablish the connection to the resources.
    • -
    • The Port.stop method, which is responsible stop services or disconnect from the +
    • The Port.stop method, which is responsible stop services or disconnect from the resources.
    • -
    • The Port.close method, which is responsible to destroy the services, resources or +
    • The Port.close method, which is responsible to destroy the services, resources or perform a simple disconnection.
    • -
    • The Port.state property, a boolean value that indicates if the port is connected +
    • The Port.state property, a boolean value that indicates if the port is connected healthy (true) or not (false).
    • -
    • The Port.client property, that return the PortClient instance that is used to +
    • The Port.client property, that return the PortClient instance that is used to interact with the resources.
    -

    class diagram

    -

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit events to notify the status of the port:

    • error: should be emitted to notify errors in the resource management or access, this will not change the provider state, but the error will be registered in the observability layers.
    • closed: should be emitted if the access to the resources is not longer possible. This -event should not be emitted if Port.stop or Port.close methods are used.
    • +event should not be emitted if Port.stop or Port.close methods are used.
    • unhealthy: should be emitted when the port has limited access to the resources.
    • healthy: should be emitted when the port has recovered the access to the resources.
    -

    class diagram

    +

    class diagram

    Check some examples of implementation in:

    -

    Hierarchy (view full)

    Constructors

    Accessors

    Methods

    Constructors

    • Implementation of functionalities of an Elastic port instance.

      +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Client
    • Return the underlying port instance

      -

      Returns Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      -

      Returns boolean

    Methods

    • Close the port instance

      -

      Returns Promise<void>

    • Initialize the port instance

      -

      Returns Promise<void>

    • Stop the port instance

      -

      Returns Promise<void>

    +
  • logger: LoggerInstance

    Port logger, to be used internally

    +
  • Returns Elastic.Port

    Accessors

    • get client(): Client
    • Return the underlying port instance

      +

      Returns Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_faker.Factory.html b/docs/classes/_mdf.js_faker.Factory.html new file mode 100644 index 00000000..9e40ee3f --- /dev/null +++ b/docs/classes/_mdf.js_faker.Factory.html @@ -0,0 +1,128 @@ +Factory | @mdf.js

    Class Factory<T, R>

    Factory for building JavaScript objects, mostly useful for setting up test data

    +

    Type Parameters

    Constructors

    Methods

    • Register a callback function to be called after the object is generated. The callback function +receives the generated object as first argument and the resolved options as second argument.

      +

      Parameters

      • callback: (object: T, options: R) => T

        Callback function

        +

      Returns Factory<T, R>

      factory.after((user) => {
      user.name = user.name.toUpperCase();
      }); +
      + +
    • Define an attribute on this factory

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +

      Returns Factory<T, R>

      factory.attr('name');
      +
      + +
    • Define an attribute on this factory using a default value (e.g. a string or number)

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +
      • defaultValue: DefaultValue<T, K>

        Default value of attribute

        +

      Returns Factory<T, R>

      factory.attr('name', 'John Doe');
      +
      + +
    • Define an attribute on this factory using a generator function

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +
      • generator: Builder<T, K>

        Value generator function

        +

      Returns Factory<T, R>

      factory.attr('name', () => function() { return 'John Doe'; });
      +
      + +
    • Define an attribute on this factory using a generator function and dependencies on options or +other attributes

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +
      • dependencies: Dependencies<T, K>

        Array of dependencies as option or attribute names that are used by the +generator function to generate the value of this attribute

        +
      • generator: Builder<T, K>

        Value generator function. The generator function will be called with the +resolved values of the dependencies as arguments.

        +

      Returns Factory<T, R>

      factory.attr('name', ['firstName', 'lastName'], (firstName, lastName) => {
      return `${firstName} ${lastName}`;
      }); +
      + +
    • Define multiple attributes on this factory using a default value (e.g. a string or number) or +generator function. If you need to define dependencies on options or other attributes, use the +attr method instead.

      +

      Parameters

      • attributes: { [K in string | number | symbol]: DefaultValue<T, K> | Builder<T, K> }

        Object with multiple attributes

        +

      Returns Factory<T, R>

      factory.attrs({
      name: 'John Doe',
      age: function() { return 21; },
      }); +
      + +
    • Returns an object that is generated by the factory. +The optional option likelihood is a number between 0 and 100 that defines the probability +that the generated object contains wrong data. This is useful for testing if your code can +handle wrong data. The default value is 100, which means that the generated object always +contains correct data.

      +

      Parameters

      • attributes: { [K in string | number | symbol]?: T[K] } = {}

        object containing attribute override key value pairs

        +
      • options: { likelihood?: number; [key: string]: any } = ...

        object containing option key value pairs

        +

      Returns T

    • Returns an array of objects that are generated by the factory. +The optional option likelihood is a number between 0 and 100 that defines the probability +that the generated object contains wrong data. This is useful for testing if your code can +handle wrong data. The default value is 100, which means that the generated object always +contains correct data.

      +

      Parameters

      • size: number

        number of objects to generate

        +
      • attributes: { [K in string | number | symbol]?: T[K] } = {}

        object containing attribute override key value pairs

        +
      • options: { likelihood?: number; [key: string]: any } = ...

        object containing option key value pairs

        +

      Returns T[]

      factory.buildList(3, { name: 'John Doe' });
      +
      + +
    • Extend this factory with another factory. The attributes and options of the other factory are +merged into this factory. If an attribute or option with the same name already exists, it is +overwritten.

      +

      Type Parameters

      • P extends Partial<T>
      • J extends Partial<R>

      Parameters

      • factory: Factory<P, J>

        Factory to extend this factory with

        +

      Returns Factory<T, R>

    • Define an option for this factory using a default value. Options are values that are not +directly used in the generated object, but can be used to influence the generation process. +For example, you could define an option withAddress that, when set to true, would generate +an address and add it to the generated object. Like attributes, options can have dependencies +on other options but not on attributes.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • opt: K

        Name of option

        +
      • defaultValue: DefaultValue<R, K>

        Default value of option

        +

      Returns Factory<T, R>

      factory.option('withAddress', false);
      +
      + +
    • Define an option for this factory using a generator function. Options are values that are not +directly used in the generated object, but can be used to influence the generation process. +For example, you could define an option withAddress that, when set to true, would generate +an address and add it to the generated object. Like attributes, options can have dependencies +on other options but not on attributes.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • opt: K

        Name of option

        +
      • generator: Builder<R, K>

        Value generator function

        +

      Returns Factory<T, R>

      factory.option('withAddress', () => function() { return false; });
      +
      + +
    • Define an option for this factory using a generator function with dependencies in other +options. Options are values that are not directly used in the generated object, but can be +used to influence the generation process. For example, you could define an option +withAddress that, when set to true, would generate an address and add it to the generated +object. Like attributes, options can have dependencies on other options but not on attributes.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • opt: K

        Name of option

        +
      • dependencies: Dependencies<R, K>

        Array of dependencies as option names that are used by the generator +function to generate the value of this option

        +
      • generator: Builder<R, K>

        Value generator function with dependencies in other options. The generator +function will be called with the resolved values of the dependencies as arguments.

        +

      Returns Factory<T, R>

    • Reset all the sequences of this factory

      +

      Returns void

    • Define an auto incrementing sequence attribute of the object. Default value is 1.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +

      Returns Factory<T, R>

      factory.sequence('id');
      +
      + +
    • Define an auto incrementing sequence attribute of the object where the sequence value is +generated by a generator function that is called with the current sequence value as argument.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +
      • generator: Builder<T, K>

        Value generator function

        +

      Returns Factory<T, R>

      factory.sequence('id', (i) => function() { return i + 11; });
      +
      + +
    • Define an auto incrementing sequence attribute of the object where the sequence value is +generated by a generator function that is called with the current sequence value as argument +and dependencies on options or other attributes.

      +

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • attr: K

        Name of attribute

        +
      • dependencies: (string | K)[]

        Array of dependencies as option or attribute names that are used by the +generator function to generate the value of the sequence attribute

        +
      • generator: Builder<T, K>

        Value generator function

        +

      Returns Factory<T, R>

      factory.sequence('id', ['idPrefix'], (i, idPrefix) => function() {
      return `${idPrefix}${i}`;
      }); +
      + +
    diff --git a/docs/classes/_mdf.js_file-flinger.FileFlinger.html b/docs/classes/_mdf.js_file-flinger.FileFlinger.html new file mode 100644 index 00000000..d0a11aff --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger.FileFlinger.html @@ -0,0 +1,37 @@ +FileFlinger | @mdf.js

    FileFlinger class +Allows to create a file processing service that can be used to watch a folder for new files and +send them to a destination (S3, FTP, ...) using pushers. +The service can be configured with a pattern to match the file names and generate keys for the +destination. +Once a file is processed, it can be moved to an archive folder (zipped optionally) or deleted. +As a @mdf.js service, it offer prometheus metrics, health checks, and logging.

    +

    Hierarchy

    • EventEmitter
      • FileFlinger

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Events

    on +

    Constructors

    Properties

    componentId: string = ...

    The component identifier

    +
    name: string

    The name of the instance of the FileFlinger

    +

    Accessors

    • get metrics(): Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Return the metrics registry

      +

      Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the file flinger

      +

      Returns Promise<void>

    • Start the file flinger

      +

      Returns Promise<void>

    • Stop the file flinger

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Multi | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component status changes.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_file-flinger._internal_.Engine.html b/docs/classes/_mdf.js_file-flinger._internal_.Engine.html new file mode 100644 index 00000000..3a46fb44 --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger._internal_.Engine.html @@ -0,0 +1,32 @@ +Engine | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • Engine

    Implements

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get componentId(): string
    • Get the component identifier

      +

      Returns string

    • get metrics(): Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Return the metrics registry

      +

      Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Get the name of the watcher

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the file flinger

      +

      Returns Promise<void>

    • Add a file to the be processed

      +

      Parameters

      • filePath: string

      Returns Promise<void>

    • Start the file flinger

      +

      Returns Promise<void>

    • Stop the file flinger

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_file-flinger._internal_.FileTasks.html b/docs/classes/_mdf.js_file-flinger._internal_.FileTasks.html new file mode 100644 index 00000000..6a4c81fa --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger._internal_.FileTasks.html @@ -0,0 +1,17 @@ +FileTasks | @mdf.js

    File tasks to process files

    +

    Constructors

    Properties

    erroredFiles: Map<string, ErroredFile> = ...

    Errored file tracks

    +

    Methods

    • Get the task to process the errored file based on the error strategy.

      +

      Parameters

      • filePath: string

        The path of the file to process

        +
      • error: unknown

        The error to process

        +

      Returns Single<void, FileTasks>

      The task to process the errored file

      +
    • Get the task to process the file.

      +

      Parameters

      • filePath: string

        The path of the file to process

        +
      • key: string

        The key to use for the file

        +

      Returns Sequence<any, FileTasks>

      The task to process the file

      +
    diff --git a/docs/classes/_mdf.js_file-flinger._internal_.Keygen.html b/docs/classes/_mdf.js_file-flinger._internal_.Keygen.html new file mode 100644 index 00000000..eaedf38c --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger._internal_.Keygen.html @@ -0,0 +1,35 @@ +Keygen | @mdf.js

    Key generator for the files +The key generator is used to generate a key for a file based on a given pattern. +The key pattern can contain placeholders that will be replaced by the actual values. +The placeholders are enclosed in curly braces and the following are supported by default:

    +
      +
    • {_filename}: The name of the file
    • +
    • {_extension}: The extension of the file
    • +
    • {_timestamp}: The timestamp when the file was processed in milliseconds
    • +
    • {_date}: The date when the file was processed in the format YYYY-MM-DD
    • +
    • {_time}: The time when the file was processed in the format HH-mm-ss
    • +
    • {_datetime}: The date and time when the file was processed in the format YYYY-MM-DD_HH-mm-ss
    • +
    • {_year}: The year when the file was processed
    • +
    • {_month}: The month when the file was processed
    • +
    • {_day}: The day when the file was processed
    • +
    • {_hour}: The hour when the file was processed
    • +
    • {_minute}: The minute when the file was processed
    • +
    • {_second}: The second when the file was processed
    • +
    +

    Other placeholders can be added by appling a custom file pattern over the file name, and using default values:

    +
      +
    • file name: mySensor_flowMeter1_2024-12-30_2024-12-31.jsonl
    • +
    • file pattern: {sensor}{measurement}{year}-{month}-{day}_{end}
    • +
    • default values: {source: 'myFileFlinger1'}
    • +
    • key pattern: {sensor}/{measurement}/{year}/{month}/{day}/data_{source}
    • +
    • key: mySensor/flowMeter1/2024/12/30/data_myFileFlinger1
    • +
    +

    Constructors

    Methods

    Constructors

    Methods

    • Generates a key for a file based on a given pattern.

      +

      Parameters

      • filePath: string

        The path to the file.

        +

      Returns string

      The generated key.

      +
    diff --git a/docs/classes/_mdf.js_file-flinger._internal_.MetricsHandler.html b/docs/classes/_mdf.js_file-flinger._internal_.MetricsHandler.html new file mode 100644 index 00000000..6fdc1dd4 --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger._internal_.MetricsHandler.html @@ -0,0 +1,7 @@ +MetricsHandler | @mdf.js

    Metrics handler

    +

    Constructors

    Properties

    Constructors

    Properties

    registry: Registry<"text/plain; version=0.0.4; charset=utf-8">

    The registry to register the metrics

    +
    diff --git a/docs/classes/_mdf.js_file-flinger._internal_.Watcher.html b/docs/classes/_mdf.js_file-flinger._internal_.Watcher.html new file mode 100644 index 00000000..86adf522 --- /dev/null +++ b/docs/classes/_mdf.js_file-flinger._internal_.Watcher.html @@ -0,0 +1,37 @@ +Watcher | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • Watcher

    Implements

    Constructors

    Accessors

    Methods

    Events

    on +

    Constructors

    Accessors

    • get componentId(): string
    • Get the component identifier

      +

      Returns string

    • get errors(): string[]
    • Get the error stack

      +

      Returns string[]

    • get name(): string
    • Get the name of the watcher

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Get the status of the watcher

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the watcher

      +

      Returns Promise<void>

    • Start the watcher

      +

      Returns Promise<void>

    • Stop the watcher

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Multi | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component status changes.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the add event, emitted when a file is added.

      +

      Parameters

      • event: "add"

        add event

        +
      • listener: (path: string) => void

        Add event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_firehose.Firehose.html b/docs/classes/_mdf.js_firehose.Firehose.html new file mode 100644 index 00000000..1a369b58 --- /dev/null +++ b/docs/classes/_mdf.js_firehose.Firehose.html @@ -0,0 +1,84 @@ +Firehose | @mdf.js

    Class Firehose<Type, Data, CustomHeaders, CustomOptions>

    Firehose class +Allows to create a firehose(DTL pipeline) instance to manage the flow of jobs between sources and +sinks. Sinks are the final destination of the jobs, sources are the origin of the jobs and the +engine is the processing unit that manages the flow of jobs between sources and sinks applying +strategies to the jobs.

    +

    Type Parameters

    • Type extends string = string

      Job type, used as selector for strategies in job processors

      +
    • Data = any

      Job payload

      +
    • CustomHeaders extends Record<string, any> = NoMoreHeaders

      Custom headers, used to pass specific information for job processors

      +
    • CustomOptions extends Record<string, any> = NoMoreOptions

      Custom options, used to pass specific information for job processors

      +

    Hierarchy

    • EventEmitter
      • Firehose

    Implements

    Constructors

    Properties

    componentId: string = ...

    Provider unique identifier for trace purposes

    +
    name: string

    Firehose name

    +

    Accessors

    • get metrics(): Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Return the metrics registry

      +

      Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Stop and close all the streams

      +

      Returns Promise<void>

    • Perform the restart of the firehose

      +

      Returns Promise<void>

    • Perform the piping of all the streams

      +

      Returns Promise<void>

    • Stop the active sink and source and unpipe them from the engine

      +

      Returns Promise<void>

    Events

    • Register an event listener over the done event, which is emitted when a job has ended, either +due to completion or failure.

      +

      Parameters

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Multi | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component status changes.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Register an event listener over the job event, which is emitted when a new job is received +from a source.

      +

      Parameters

      Returns this

    • Register an event listener over the done event, which is emitted when a job has ended, either +due to completion or failure.

      +

      Parameters

      Returns this

    • Register an event listener over the hold event, which is emitted when the engine is paused due +to inactivity.

      +

      Parameters

      • event: "hold"

        restart event

        +
      • listener: () => void

        Hold event listener

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a job has +ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a job has ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a job has ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Base-1.html b/docs/classes/_mdf.js_firehose._internal_.Base-1.html new file mode 100644 index 00000000..a1fc13af --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Base-1.html @@ -0,0 +1,24 @@ +Base | @mdf.js

    Firehose source (Readable) plug class

    +

    Type Parameters

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the read of data from the source

      +

      Parameters

      • size: number

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Base.html b/docs/classes/_mdf.js_firehose._internal_.Base.html new file mode 100644 index 00000000..627cfbfa --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Base.html @@ -0,0 +1,22 @@ +Base | @mdf.js

    Firehose sink (Writable) plug class

    +

    Type Parameters

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.CreditsFlow.html b/docs/classes/_mdf.js_firehose._internal_.CreditsFlow.html new file mode 100644 index 00000000..c224c5c8 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.CreditsFlow.html @@ -0,0 +1,24 @@ +CreditsFlow | @mdf.js

    Firehose source (Readable) plug class

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the read of data from the source

      +

      Parameters

      • size: number

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Engine.html b/docs/classes/_mdf.js_firehose._internal_.Engine.html new file mode 100644 index 00000000..8c3b7ffb --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Engine.html @@ -0,0 +1,41 @@ +Engine | @mdf.js

    A component is any part of the system that has a own identity and can be monitored for error +handling. The only requirement is to emit an error event when something goes wrong, to have a +name and unique component identifier.

    +

    Hierarchy

    • Transform
      • Engine

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Create a new instance of the datapoint filter stream

      +

      Parameters

      • name: string

        name of the transform

        +
      • Optionaloptions: EngineOptions

        engine options

        +

      Returns Engine

    Properties

    componentId: string = ...

    Provider unique identifier for trace purposes

    +
    name: string

    name of the transform

    +

    Accessors

    Methods

    • Perform the filter strategy depending of the job type

      +

      Parameters

      • job: OpenJobHandler

        publication job object

        +
      • encoding: string

        encoding of the job

        +
      • callback: (error?: Crash, chunk?: any) => void

        callback to return the job

        +

      Returns void

    • Close the stream and release resources

      +

      Returns void

    • Emitted when it is appropriate to resume writing data to the stream

      +

      Parameters

      • event: "drain"
      • listener: () => void

      Returns this

    • Emitted when stream.resume() is called and readableFlowing is not true

      +

      Parameters

      • event: "resume"
      • listener: () => void

      Returns this

    • Emitted when there is data available to be read from the stream

      +

      Parameters

      • event: "readable"
      • listener: () => void

      Returns this

    • Emitted when stream.pause() is called and readableFlowing is not false

      +

      Parameters

      • event: "pause"
      • listener: () => void

      Returns this

    • Due to the implementation of consumer classes, this event will never emitted

      +

      Parameters

      • event: "error"
      • listener: (error: Crash | Error) => void

      Returns this

    • Emitted when there is no more data to be consumed from the stream

      +

      Parameters

      • event: "end"
      • listener: () => void

      Returns this

    • Emitted whenever the stream is relinquishing ownership of a chunk of data to a consumer

      +

      Parameters

      • event: "data"
      • listener: (chunk: any) => void

      Returns this

    • Emitted when the stream have been closed

      +

      Parameters

      • event: "close"
      • listener: () => void

      Returns this

    • Emitted on every state change

      +

      Parameters

      • event: "status"
      • listener: (providerState: "pass" | "fail" | "warn") => void

      Returns this

    • Emitted when the stream is finished

      +

      Parameters

      • event: "finish"
      • listener: () => void

      Returns this

    • Emitted when the stream is piped with a readable stream

      +

      Parameters

      • event: "pipe"
      • listener: (source: Readable) => void

      Returns this

    • Emitted when the stream is piped with a readable stream

      +

      Parameters

      • event: "unpipe"
      • listener: (source: Readable) => void

      Returns this

    • Emitted when the inactivity time exceeds the limit

      +

      Parameters

      • event: "hold"
      • listener: () => void

      Returns this

    • Start the stream

      +

      Returns void

    • Stop the stream

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Flow.html b/docs/classes/_mdf.js_firehose._internal_.Flow.html new file mode 100644 index 00000000..5458fa3e --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Flow.html @@ -0,0 +1,24 @@ +Flow | @mdf.js

    Firehose source (Readable) plug class

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the read of data from the source

      +

      Parameters

      • size: number

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Jet.html b/docs/classes/_mdf.js_firehose._internal_.Jet.html new file mode 100644 index 00000000..1be03535 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Jet.html @@ -0,0 +1,26 @@ +Jet | @mdf.js

    Firehose sink (Writable) plug class

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the publication of the information on the sink destination

      +

      Parameters

      Returns void

    • Perform the publication of the information on the sink destination

      +

      Parameters

      • data: { chunk: OpenJobHandler; encoding: BufferEncoding }[]
      • callback: (error?: Crash | Error) => void

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.MetricsHandler.html b/docs/classes/_mdf.js_firehose._internal_.MetricsHandler.html new file mode 100644 index 00000000..57ff5abd --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.MetricsHandler.html @@ -0,0 +1,9 @@ +MetricsHandler | @mdf.js

    Metrics handler

    +

    Constructors

    Properties

    Methods

    Constructors

    Properties

    registry: Registry<"text/plain; version=0.0.4; charset=utf-8">

    The registry to register the metrics

    +

    Methods

    • Register the metrics handler to a firehose source

      +

      Parameters

      Returns void

    diff --git a/docs/classes/_mdf.js_firehose._internal_.PlugWrapper-1.html b/docs/classes/_mdf.js_firehose._internal_.PlugWrapper-1.html new file mode 100644 index 00000000..d605f9c8 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.PlugWrapper-1.html @@ -0,0 +1,19 @@ +PlugWrapper | @mdf.js

    A component is any part of the system that has a own identity and can be monitored for error +handling. The only requirement is to emit an error event when something goes wrong, to have a +name and unique component identifier.

    +

    Hierarchy

    • EventEmitter
      • PlugWrapper

    Implements

    Constructors

    Accessors

    Constructors

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    diff --git a/docs/classes/_mdf.js_firehose._internal_.PlugWrapper.html b/docs/classes/_mdf.js_firehose._internal_.PlugWrapper.html new file mode 100644 index 00000000..e36e9570 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.PlugWrapper.html @@ -0,0 +1,18 @@ +PlugWrapper | @mdf.js

    A component is any part of the system that has a own identity and can be monitored for error +handling. The only requirement is to emit an error event when something goes wrong, to have a +name and unique component identifier.

    +

    Hierarchy

    • EventEmitter
      • PlugWrapper

    Implements

    Constructors

    Accessors

    Constructors

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Sequence.html b/docs/classes/_mdf.js_firehose._internal_.Sequence.html new file mode 100644 index 00000000..f8a09766 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Sequence.html @@ -0,0 +1,24 @@ +Sequence | @mdf.js

    Firehose source (Readable) plug class

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the read of data from the source

      +

      Parameters

      • size: number

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_firehose._internal_.Tap.html b/docs/classes/_mdf.js_firehose._internal_.Tap.html new file mode 100644 index 00000000..eb750a02 --- /dev/null +++ b/docs/classes/_mdf.js_firehose._internal_.Tap.html @@ -0,0 +1,24 @@ +Tap | @mdf.js

    Firehose sink (Writable) plug class

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    plugWrapper: PlugWrapper

    Wrapped source plug

    +

    Accessors

    • get componentId(): string
    • Component identification

      +

      Returns string

    • get metrics(): undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Metrics registry for this component

      +

      Returns undefined | Registry<"text/plain; version=0.0.4; charset=utf-8">

    • get name(): string
    • Component name

      +

      Returns string

    Methods

    • Perform the publication of the information on the sink destination

      +

      Parameters

      Returns void

    • Start the Plug and the underlayer resources, making it available

      +

      Returns Promise<void>

    • Stop the Plug and the underlayer resources, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_http-client-provider.HTTP.Port.html b/docs/classes/_mdf.js_http-client-provider.HTTP.Port.html new file mode 100644 index 00000000..979772c0 --- /dev/null +++ b/docs/classes/_mdf.js_http-client-provider.HTTP.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): AxiosInstance
    • Return the underlying port instance

      +

      Returns AxiosInstance

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_http-server-provider.HTTP.Port.html b/docs/classes/_mdf.js_http-server-provider.HTTP.Port.html new file mode 100644 index 00000000..4a31408b --- /dev/null +++ b/docs/classes/_mdf.js_http-server-provider.HTTP.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Server<typeof IncomingMessage, typeof ServerResponse>
    • Return the underlying port instance

      +

      Returns Server<typeof IncomingMessage, typeof ServerResponse>

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_jsonl-archiver.ArchiverManager.html b/docs/classes/_mdf.js_jsonl-archiver.ArchiverManager.html new file mode 100644 index 00000000..2d162d28 --- /dev/null +++ b/docs/classes/_mdf.js_jsonl-archiver.ArchiverManager.html @@ -0,0 +1,33 @@ +ArchiverManager | @mdf.js

    Class responsible of managing jsonl file store operations

    +

    Hierarchy

    • EventEmitter
      • ArchiverManager

    Constructors

    Accessors

    Methods

    Events

    on +

    Constructors

    Accessors

    • get stats(): Record<string, FileStats>
    • Returns the statistics of the files

      +

      Returns Record<string, FileStats>

    Methods

    • Appends data to a JSONL file.

      +

      Parameters

      • data: Record<string, any> | Record<string, any>[]

        Data to append

        +

      Returns Promise<AppendResult>

    • Appends data to a JSONL file.

      +

      Parameters

      • data: Record<string, any> | Record<string, any>[]

        Data to append

        +
      • Optionalfilename: string

        Name of the file to append data to

        +

      Returns Promise<AppendResult>

    • Returns the error status of the file handlers

      +

      Returns boolean

    • Starts the ArchiverManager

      +

      Returns Promise<void>

    • Stops the jsonl file store manager

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when there is an error in a file handler +operation.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash) => void

        Error event listener

        +

      Returns this

    • Add a listener for the rotate event, emitted when a file is rotated.

      +

      Parameters

      • event: "rotate"

        error event

        +
      • listener: (stats: FileStats) => void

        Error event listener

        +

      Returns this

    • Add a listener for the resolve event, emitted when an operation is resolved.

      +

      Parameters

      • event: "resolve"

        resolve event

        +
      • listener: (stats: FileStats) => void

        Resolve event listener

        +

      Returns this

    • Adds a listener for the handlerCleaned event, emitted when a handler is cleaned up due to inactivity.

      +

      Parameters

      • event: "handlerCleaned"

        handlerCleaned event

        +
      • listener: (handlerName: string) => void

        Handler cleaned event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_jsonl-archiver.JSONLArchiver.Port.html b/docs/classes/_mdf.js_jsonl-archiver.JSONLArchiver.Port.html new file mode 100644 index 00000000..f4bbe432 --- /dev/null +++ b/docs/classes/_mdf.js_jsonl-archiver.JSONLArchiver.Port.html @@ -0,0 +1,59 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_jsonl-archiver._internal_.FileHandler.html b/docs/classes/_mdf.js_jsonl-archiver._internal_.FileHandler.html new file mode 100644 index 00000000..ab9460c3 --- /dev/null +++ b/docs/classes/_mdf.js_jsonl-archiver._internal_.FileHandler.html @@ -0,0 +1,35 @@ +FileHandler | @mdf.js

    File handler class for managing file operations

    +

    Hierarchy

    • EventEmitter
      • FileHandler

    Constructors

    Properties

    Methods

    Events

    on +

    Constructors

    Properties

    name: string

    Service name

    +

    Service setup options

    +
    rotationScheduled: boolean = false

    Flag to indicate if a rotation is scheduled

    +
    stats: FileStats = ...

    File stats

    +

    Methods

    • Appends data to the file store. +This method uses the Limiter to ensure only one file operation is executed +at a time (concurrency=1) and to retry the append operation if necessary

      +

      Parameters

      • data: string

        The data to append to the file.

        +

      Returns Promise<void>

      A promise that resolves when the append operation is complete.

      +
    • Closes the current file stream.

      +

      Returns Promise<void>

      A promise that resolves when the file stream is closed.

      +

    Events

    • Add a listener for the error event, emitted when there is an error in a file handler +operation.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash) => void

        Error event listener

        +

      Returns this

    • Add a listener for the rotate event, emitted when a file is rotated.

      +

      Parameters

      • event: "rotate"

        error event

        +
      • listener: (stats: FileStats) => void

        Error event listener

        +

      Returns this

    • Add a listener for the resolve event, emitted when an operation is resolved.

      +

      Parameters

      • event: "resolve"

        resolve event

        +
      • listener: (stats: FileStats) => void

        Resolve event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_kafka-provider.Consumer.Port.html b/docs/classes/_mdf.js_kafka-provider.Consumer.Port.html new file mode 100644 index 00000000..b6665086 --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider.Consumer.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Client
    • Return the underlying port instance

      +

      Returns Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_kafka-provider.Producer.Port.html b/docs/classes/_mdf.js_kafka-provider.Producer.Port.html new file mode 100644 index 00000000..3cf92ebc --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider.Producer.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Client
    • Return the underlying port instance

      +

      Returns Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_kafka-provider._internal_.BasePort.html b/docs/classes/_mdf.js_kafka-provider._internal_.BasePort.html new file mode 100644 index 00000000..c8e539a3 --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider._internal_.BasePort.html @@ -0,0 +1,61 @@ +BasePort | @mdf.js

    Class BasePort<Client, Config>Abstract

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Type Parameters

    • Client

      Underlying client type, this is, the real client of the wrapped provider

      +
    • Config extends BaseConfig

      Port configuration object, could be an extended version of the client config

      +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Client
    • Return the underlying port instance

      +

      Returns Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_kafka-provider._internal_.Client.html b/docs/classes/_mdf.js_kafka-provider._internal_.Client.html new file mode 100644 index 00000000..9c1863cc --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider._internal_.Client.html @@ -0,0 +1,19 @@ +Client | @mdf.js

    This client is based in the functionality of kafkajs for NodeJS. +In the moment of implement the port, this library show some issues and not clear API about how +the connection handshake and events are managed by the library, for this reason we decide to +include an admin client in all the cases, even when we only want to stablish a consumer or +producer.

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    on +

    Constructors

    • Create an instance of a Kafka client configuration options

      +

      Parameters

      • options: KafkaConfig

        Kafka client configuration options

        +
      • interval: number = DEFAULT_CHECK_INTERVAL

        Period of health check interval

        +

      Returns Client

    Properties

    options: KafkaConfig

    Kafka Broker configuration options

    +

    Accessors

    • get state(): boolean
    • Overall client state

      +

      Returns boolean

    Methods

    • Emitted when admin client can collect the desired information

      +

      Parameters

      • event: "healthy"
      • listener: () => void

      Returns this

    • Emitted when admin client can not collect the desired information

      +

      Parameters

      • event: "unhealthy"
      • listener: (crash: Crash) => void

      Returns this

    • Emitted every time that admin client get metadata from brokers

      +

      Parameters

      Returns this

    • Emitted when admin client has some problem getting the metadata from brokers

      +

      Parameters

      • event: "error"
      • listener: (crash: Crash) => void

      Returns this

    diff --git a/docs/classes/_mdf.js_kafka-provider._internal_.Consumer.html b/docs/classes/_mdf.js_kafka-provider._internal_.Consumer.html new file mode 100644 index 00000000..60cb9d83 --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider._internal_.Consumer.html @@ -0,0 +1,28 @@ +Consumer | @mdf.js

    This client is based in the functionality of kafkajs for NodeJS. +In the moment of implement the port, this library show some issues and not clear API about how +the connection handshake and events are managed by the library, for this reason we decide to +include an admin client in all the cases, even when we only want to stablish a consumer or +producer.

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Creates an instance of KafkaConsumer

      +

      Parameters

      • clientOptions: KafkaConfig

        Kafka client configuration options

        +
      • consumerOptions: ConsumerConfig

        Kafka consumer configuration options

        +
      • Optionalinterval: number

        Period of health check interval

        +

      Returns Consumer

    Properties

    consumerOptions: ConsumerConfig

    Kafka Consumer configuration options

    +
    options: KafkaConfig

    Kafka Broker configuration options

    +

    Accessors

    • get client(): Consumer
    • Kafka consumer

      +

      Returns Consumer

    • get state(): boolean
    • Overall client state

      +

      Returns boolean

    Methods

    • Emitted when admin client can collect the desired information

      +

      Parameters

      • event: "healthy"
      • listener: () => void

      Returns this

    • Emitted when admin client can not collect the desired information

      +

      Parameters

      • event: "unhealthy"
      • listener: (crash: Crash) => void

      Returns this

    • Emitted every time that admin client get metadata from brokers

      +

      Parameters

      Returns this

    • Emitted when admin client has some problem getting the metadata from brokers

      +

      Parameters

      • event: "error"
      • listener: (crash: Crash) => void

      Returns this

    • Perform the connection of the instance to the system

      +

      Returns Promise<void>

    • Perform the disconnection of the instance from the system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_kafka-provider._internal_.Producer.html b/docs/classes/_mdf.js_kafka-provider._internal_.Producer.html new file mode 100644 index 00000000..d03e970d --- /dev/null +++ b/docs/classes/_mdf.js_kafka-provider._internal_.Producer.html @@ -0,0 +1,26 @@ +Producer | @mdf.js

    This client is based in the functionality of kafkajs for NodeJS. +In the moment of implement the port, this library show some issues and not clear API about how +the connection handshake and events are managed by the library, for this reason we decide to +include an admin client in all the cases, even when we only want to stablish a consumer or +producer.

    +

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Creates an instance of KafkaProducer

      +

      Parameters

      • clientOptions: KafkaConfig

        Kafka client configuration options

        +
      • OptionalproducerOptions: ProducerConfig

        Kafka producer configuration options

        +
      • Optionalinterval: number

        Period of health check interval

        +

      Returns Producer

    Properties

    options: KafkaConfig

    Kafka Broker configuration options

    +

    Accessors

    • get client(): Producer
    • Return the producer of this class instance

      +

      Returns Producer

    • get state(): boolean
    • Overall client state

      +

      Returns boolean

    Methods

    • Emitted when admin client can collect the desired information

      +

      Parameters

      • event: "healthy"
      • listener: () => void

      Returns this

    • Emitted when admin client can not collect the desired information

      +

      Parameters

      • event: "unhealthy"
      • listener: (crash: Crash) => void

      Returns this

    • Emitted every time that admin client get metadata from brokers

      +

      Parameters

      Returns this

    • Emitted when admin client has some problem getting the metadata from brokers

      +

      Parameters

      • event: "error"
      • listener: (crash: Crash) => void

      Returns this

    • Perform the connection of the instance to the system

      +

      Returns Promise<void>

    • Perform the disconnection of the instance from the system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_logger.DebugLogger.html b/docs/classes/_mdf.js_logger.DebugLogger.html new file mode 100644 index 00000000..dc77a46f --- /dev/null +++ b/docs/classes/_mdf.js_logger.DebugLogger.html @@ -0,0 +1,55 @@ +DebugLogger | @mdf.js

    Represents a logger instance with different log levels and functions.

    +

    Implements

    Constructors

    Properties

    Methods

    Constructors

    • Creates a new instance of the default logger

      +

      Parameters

      • name: string

        Name of the provider

        +

      Returns DebugLogger

    Properties

    debug: LoggerFunction = ...

    Log events in the DEBUG level: all the information in a detailed way. +This level used to be necessary only in the debugging process, so not all the data is +reported, only the related with the main processes and tasks.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    error: LoggerFunction = ...

    Log events in the ERROR level: all the errors and problems with detailed information.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    info: LoggerFunction = ...

    Log events in the INFO level: only relevant events are reported. +This level is the default level.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    silly: LoggerFunction = ...

    Log events in the SILLY level: all the information in a very detailed way. +This level used to be necessary only in the development process, and the meta data used to be +the results of the operations.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    stream: { write: (message: string) => void } = ...

    Stream logger

    +
    verbose: LoggerFunction = ...

    Log events in the VERBOSE level: trace information without details. +This level used to be necessary only in system configuration process, so information about +the settings and startup process used to be reported.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    warn: LoggerFunction = ...

    Log events in the WARN level: information about possible problems or dangerous situations.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +

    Methods

    • Log events in the ERROR level: all the information in a very detailed way. +This level used to be necessary only in the development process.

      +

      Parameters

      • rawError: Crash | Boom | Multi

        crash error instance

        +
      • Optionalcontext: string

        context (class/function) where this logger is logging

        +

      Returns void

    diff --git a/docs/classes/_mdf_js_logger.Logger.html b/docs/classes/_mdf.js_logger.Logger.html similarity index 51% rename from docs/classes/_mdf_js_logger.Logger.html rename to docs/classes/_mdf.js_logger.Logger.html index ea589ee3..d5993516 100644 --- a/docs/classes/_mdf_js_logger.Logger.html +++ b/docs/classes/_mdf.js_logger.Logger.html @@ -1,69 +1,69 @@ -Logger | @mdf.js

    Class Logger, manage the event and log register process for netin artifacts

    -

    Implements

    Constructors

    • Create a netin logger instance with default values

      -

      Returns Logger

    • Create a netin logger instance with default values

      +Logger | @mdf.js

      Class Logger, manage the event and log register process for netin artifacts

      +

      Implements

      Constructors

      • Create a netin logger instance with default values

        +

        Returns Logger

      • Create a netin logger instance with default values

        Parameters

        • label: string

          logger label

          -

        Returns Logger

      • Create a netin logger instance with default values

        +

      Returns Logger

    • Create a netin logger instance with default values

      Parameters

      • label: string

        logger label

        -
      • Optionalconfiguration: LoggerConfig

        logger configuration

        -

      Returns Logger

    Accessors

    • get configError(): undefined | Multi
    • Logger configuration errors, if exist

      -

      Returns undefined | Multi

    • get hasError(): boolean
    • Logger configuration error flag

      -

      Returns boolean

    • get stream(): {
          write: ((str: string) => void);
      }
    • Stream de escritura del propio logger

      -

      Returns {
          write: ((str: string) => void);
      }

      • write: ((str: string) => void)
          • (str): void
          • Parameters

            • str: string

            Returns void

    Methods

    • Log events in the ERROR level: all the information in a very detailed way. +

    • Optionalconfiguration: LoggerConfig

      logger configuration

      +

    Returns Logger

    Accessors

    • get configError(): undefined | Multi
    • Logger configuration errors, if exist

      +

      Returns undefined | Multi

    • get hasError(): boolean
    • Logger configuration error flag

      +

      Returns boolean

    • get stream(): { write: (str: string) => void }
    • Stream de escritura del propio logger

      +

      Returns { write: (str: string) => void }

    Methods

    • Log events in the ERROR level: all the information in a very detailed way. This level used to be necessary only in the development process.

      -

      Parameters

      • error: Crash | Boom | Multi

        crash error instance

        +

        Parameters

        • error: Crash | Boom | Multi

          crash error instance

        • Optionalcontext: string

          context (class/function) where this logger is logging

          -

        Returns void

    • Log events in the DEBUG level: all the information in a detailed way. +

    Returns void

    • Log events in the DEBUG level: all the information in a detailed way. This level used to be necessary only in the debugging process, so not all the data is reported, only the related with the main processes and tasks.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    • Log events in the ERROR level: all the errors and problems with detailed information.

      +
    • ...meta: any[]

      extra information

      +

    Returns void

    • Log events in the ERROR level: all the errors and problems with detailed information.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    • Log events in the INFO level: only relevant events are reported. +

    • ...meta: any[]

      extra information

      +

    Returns void

    • Log events in the INFO level: only relevant events are reported. This level is the default level.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    • Establish the logger configuration

      +
    • ...meta: any[]

      extra information

      +

    Returns void

    • Establish the logger configuration

      Parameters

      • label: string

        Logger label

        -
      • configuration: LoggerConfig

        logger configuration

        -

      Returns void

    • Log events in the SILLY level: all the information in a very detailed way. +

    • configuration: LoggerConfig

      logger configuration

      +

    Returns void

    • Log events in the SILLY level: all the information in a very detailed way. This level used to be necessary only in the development process, and the meta data used to be the results of the operations.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    • Log events in the VERBOSE level: trace information without details. +

    • ...meta: any[]

      extra information

      +

    Returns void

    • Log events in the VERBOSE level: trace information without details. This level used to be necessary only in system configuration process, so information about the settings and startup process used to be reported.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    • Log events in the WARN level: information about possible problems or dangerous situations.

      +
    • ...meta: any[]

      extra information

      +

    Returns void

    • Log events in the WARN level: information about possible problems or dangerous situations.

      Parameters

      • message: string

        human readable information to log

      • Optionaluuid: string

        unique identifier for the actual job/task/request process

      • Optionalcontext: string

        context (class/function) where this logger is logging

        -
      • Rest...meta: any[]

        extra information

        -

      Returns void

    +
  • ...meta: any[]

    extra information

    +
  • Returns void

    diff --git a/docs/classes/_mdf.js_logger.WrapperLogger.html b/docs/classes/_mdf.js_logger.WrapperLogger.html new file mode 100644 index 00000000..3d3cf923 --- /dev/null +++ b/docs/classes/_mdf.js_logger.WrapperLogger.html @@ -0,0 +1,56 @@ +WrapperLogger | @mdf.js

    Constructors

    Properties

    Methods

    Constructors

    • Create a wrapped version of the logger where the context and uuid are already set

      +

      Parameters

      • logger: LoggerInstance

        Logger instance to wrap

        +
      • context: string

        context (class/function) where this logger is logging

        +
      • componentId: string = ...

        component identification

        +

      Returns WrapperLogger

    Properties

    debug: LoggerFunction = ...

    Log events in the DEBUG level: all the information in a detailed way. +This level used to be necessary only in the debugging process, so not all the data is +reported, only the related with the main processes and tasks.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    error: LoggerFunction = ...

    Log events in the ERROR level: all the errors and problems with detailed information.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    info: LoggerFunction = ...

    Log events in the INFO level: only relevant events are reported. +This level is the default level.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    silly: LoggerFunction = ...

    Log events in the SILLY level: all the information in a very detailed way. +This level used to be necessary only in the development process, and the meta data used to be +the results of the operations.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    stream: { write: (message: string) => void } = ...

    Stream logger

    +
    verbose: LoggerFunction = ...

    Log events in the VERBOSE level: trace information without details. +This level used to be necessary only in system configuration process, so information about +the settings and startup process used to be reported.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +
    warn: LoggerFunction = ...

    Log events in the WARN level: information about possible problems or dangerous situations.

    +

    human readable information to log

    +

    unique identifier for the actual job/task/request process

    +

    context (class/function) where this logger is logging

    +

    extra information

    +

    Methods

    • Log events in the ERROR level: all the information in a very detailed way. +This level used to be necessary only in the development process.

      +

      Parameters

      • rawError: Crash | Boom | Multi

        crash error instance

        +
      • Optionalcontext: string

        context (class/function) where this logger is logging

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_middlewares.Audit.html b/docs/classes/_mdf.js_middlewares.Audit.html new file mode 100644 index 00000000..6997a57f --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.Audit.html @@ -0,0 +1,11 @@ +Audit | @mdf.js

    Methods

    • Audit logger function

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Request audit logger middleware handler

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.AuthZ.html b/docs/classes/_mdf.js_middlewares.AuthZ.html new file mode 100644 index 00000000..321c171b --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.AuthZ.html @@ -0,0 +1,6 @@ +AuthZ | @mdf.js

    AuthZ

    +

    Constructors

    Methods

    Constructors

    Methods

    • Perform the authorization based on jwt token for Socket.IO

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.BodyParser.html b/docs/classes/_mdf.js_middlewares.BodyParser.html new file mode 100644 index 00000000..0a17ba28 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.BodyParser.html @@ -0,0 +1,18 @@ +BodyParser | @mdf.js

    Constructors

    Methods

    • Returns middleware that only parses json and only looks at requests where the Content-Type +header matches the type option.

      +

      Parameters

      • Optionaloptions: OptionsJson

        json body parser options

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Returns middleware that parses all bodies as a Buffer and only looks at requests where the +Content-Type header matches the type option.

      +

      Parameters

      • Optionaloptions: Options

        body parser options

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Returns middleware that parses all bodies as a string and only looks at requests where the +Content-Type header matches the type option.

      +

      Parameters

      • Optionaloptions: OptionsText

        text body parser options

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Returns middleware that only parses urlencoded bodies and only looks at requests where the +Content-Type header matches the type option

      +

      Parameters

      • Optionaloptions: OptionsUrlencoded

        url encoded body parser options

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf_js_middlewares.Cache.html b/docs/classes/_mdf.js_middlewares.Cache.html similarity index 51% rename from docs/classes/_mdf_js_middlewares.Cache.html rename to docs/classes/_mdf.js_middlewares.Cache.html index 6634530d..44436c6f 100644 --- a/docs/classes/_mdf_js_middlewares.Cache.html +++ b/docs/classes/_mdf.js_middlewares.Cache.html @@ -1,12 +1,12 @@ -Cache | @mdf.js

    CacheRequest middleware

    -

    Methods

    • Cache middleware function

      -

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Request cache middleware handler

      +Cache | @mdf.js

      CacheRequest middleware

      +

      Methods

      • Cache middleware function

        +

        Parameters

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Request cache middleware handler

        Parameters

        • provider: Redis

          redis client

          -
        • Optionaloptions: Partial<CacheConfig>

          Cache options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Cache middleware instance

        +
      • Optionaloptions: Partial<CacheConfig>

        Cache options

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Cache middleware instance

      Parameters

      • client: Redis

        redis client

        -
      • Optionaloptions: Partial<CacheConfig>

      Returns Cache

    +
  • Optionaloptions: Partial<CacheConfig>
  • Returns Cache

    diff --git a/docs/classes/_mdf_js_middlewares.Cors.html b/docs/classes/_mdf.js_middlewares.Cors.html similarity index 52% rename from docs/classes/_mdf_js_middlewares.Cors.html rename to docs/classes/_mdf.js_middlewares.Cors.html index 34fa3588..cb194e94 100644 --- a/docs/classes/_mdf_js_middlewares.Cors.html +++ b/docs/classes/_mdf.js_middlewares.Cors.html @@ -1,4 +1,4 @@ -Cors | @mdf.js

    CorsManagement class manages the API request CORS

    -

    Methods

    Methods

    • Return a cors handler middleware instance

      -

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    +Cors | @mdf.js

    CorsManagement class manages the API request CORS

    +

    Methods

    Methods

    • Return a cors handler middleware instance

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.Default.html b/docs/classes/_mdf.js_middlewares.Default.html new file mode 100644 index 00000000..3e357330 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.Default.html @@ -0,0 +1,8 @@ +Default | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Format the links to be used in the default handler

      +

      Parameters

      • baseRequestUrl: string

        base url where the observability is served

        +
      • links: Links = {}

        list of links to be formatted

        +

      Returns Links

    • Request traceability middleware handler

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.ErrorHandler.html b/docs/classes/_mdf.js_middlewares.ErrorHandler.html new file mode 100644 index 00000000..acaebbe9 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.ErrorHandler.html @@ -0,0 +1,5 @@ +ErrorHandler | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Return a error handler middleware instance

      +

      Parameters

      • Optionallogger: Logger

        logger instance

        +

      Returns ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.Logger.html b/docs/classes/_mdf.js_middlewares.Logger.html new file mode 100644 index 00000000..6a4e5706 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.Logger.html @@ -0,0 +1,4 @@ +Logger | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Request logger middleware handler

      +

      Parameters

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf_js_middlewares.Metrics.html b/docs/classes/_mdf.js_middlewares.Metrics.html similarity index 50% rename from docs/classes/_mdf_js_middlewares.Metrics.html rename to docs/classes/_mdf.js_middlewares.Metrics.html index 9502367a..9bf93053 100644 --- a/docs/classes/_mdf_js_middlewares.Metrics.html +++ b/docs/classes/_mdf.js_middlewares.Metrics.html @@ -1,8 +1,8 @@ -Metrics | @mdf.js

    MetricsExpressMiddleware middleware

    -

    Methods

    Methods

    • Return a metrics middleware instance

      +Metrics | @mdf.js

      MetricsExpressMiddleware middleware

      +

      Methods

      Methods

      • Return a metrics middleware instance

        Parameters

        • Optionalprefix: string

          Metrics prefix

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Return a metrics middleware instance

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Return a metrics middleware instance

      Parameters

      • Optionalregistry: Registry<"text/plain; version=0.0.4; charset=utf-8">

        Metrics registry interface

      • Optionalprefix: string

        Metrics prefix

        -

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    +

    Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.Multer.html b/docs/classes/_mdf.js_middlewares.Multer.html new file mode 100644 index 00000000..6f4091c0 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.Multer.html @@ -0,0 +1,69 @@ +Multer | @mdf.js

    Multer middleware wrapping for multipart/from-data

    +

    WARNING: +Make sure that you always handle the files that a user uploads. Never add multer as a global +middleware since a malicious user could upload files to a route that you didn't anticipate. Only +use this function on routes where you are handling the uploaded files.

    +

    Methods

    • Accepts all files that comes over the wire. An array of files will be stored in req.files.

      +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Accept an array of files, all with the name fieldName. Optionally error out if more than +maxCount files are uploaded. The array of files will be stored in req.files

      +

      Parameters

      • fieldName: string

        name of file

        +
      • OptionalmaxCount: number

        maximum number of files

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Accept a mix of files, specified by fields. An object with arrays of files will be stored in +req.files.

      +

      Parameters

      • fields: readonly Field[]

        array of entries

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]
      +
      + +
    • Accept only text fields. If any file upload is made, error with code "Unexpected field" will +be issued.

      +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Accept a single file with the name fieldName. The single file will be stored in req.file

      +

      Parameters

      • fieldName: string

        name of the file

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Return a new instance of the multipart/form-data middleware that accepts all files that comes +over the wire. An array of files will be stored in req.files.

      +

      Parameters

      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Return a new instance of the multipart/form-data middleware that accepts an array of files, all +with the name fieldName. Optionally error out if more than maxCount files are uploaded. The +array of files will be stored in req.files

      +

      Parameters

      • fieldName: string

        name of the file field

        +
      • OptionalmaxCount: number

        maximum number of files

        +
      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Return a new instance of the multipart/form-data middleware that accepts a mix of files, +specified by fields. An object with arrays of files will be stored in req.files.

      +

      Parameters

      • fields: readonly Field[]

        array of entries

        +
      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]
      +
      + +
    • Return a new instance of the multipart/form-data middleware

      +

      Parameters

      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns Multer

    • Return a new instance of the multipart/form-data middleware that accepts only text fields. If +any file upload is made, error with code "Unexpected field" will be issued.

      +

      Parameters

      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    • Return a new instance of the multipart/form-data middleware that accepts a single file with the +name fieldName. The single file will be stored in req.file

      +

      Parameters

      • fieldName: string

        name of the file field

        +
      • Optionalstorage: StorageEngine

        storage engine used for this middleware

        +
      • OptionalallowedMineTypes: string | string[]

        Allowed mime types allowed for this multer instance

        +
      • Optionallimits: {}

        Limits for the middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.NoCache.html b/docs/classes/_mdf.js_middlewares.NoCache.html new file mode 100644 index 00000000..9b37ba34 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.NoCache.html @@ -0,0 +1,4 @@ +NoCache | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Request cache middleware handler

      +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf_js_middlewares.RateLimiter.html b/docs/classes/_mdf.js_middlewares.RateLimiter.html similarity index 54% rename from docs/classes/_mdf_js_middlewares.RateLimiter.html rename to docs/classes/_mdf.js_middlewares.RateLimiter.html index 2a2f3d4c..01586be7 100644 --- a/docs/classes/_mdf_js_middlewares.RateLimiter.html +++ b/docs/classes/_mdf.js_middlewares.RateLimiter.html @@ -1,7 +1,7 @@ -RateLimiter | @mdf.js

    RateLimiter class manages the API requests rate limits

    -

    Constructors

    Methods

    get -

    Constructors

    Methods

    • Get the request handler

      -

      Parameters

      • label: string

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    +RateLimiter | @mdf.js

    RateLimiter class manages the API requests rate limits

    +

    Constructors

    Methods

    get +

    Constructors

    Methods

    • Get the request handler

      +

      Parameters

      • label: string

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.RequestId.html b/docs/classes/_mdf.js_middlewares.RequestId.html new file mode 100644 index 00000000..733930fa --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.RequestId.html @@ -0,0 +1,4 @@ +RequestId | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Request traceability middleware handler

      +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

    diff --git a/docs/classes/_mdf.js_middlewares.Security.html b/docs/classes/_mdf.js_middlewares.Security.html new file mode 100644 index 00000000..127c920e --- /dev/null +++ b/docs/classes/_mdf.js_middlewares.Security.html @@ -0,0 +1,5 @@ +Security | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    • Return an array of security middlewares

      +

      Parameters

      • enable: boolean = true

        flag to enable or disable the security middleware

        +

      Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>[]

    diff --git a/docs/classes/_mdf.js_middlewares._internal_.CacheRepository.html b/docs/classes/_mdf.js_middlewares._internal_.CacheRepository.html new file mode 100644 index 00000000..b3404c98 --- /dev/null +++ b/docs/classes/_mdf.js_middlewares._internal_.CacheRepository.html @@ -0,0 +1,14 @@ +CacheRepository | @mdf.js

    CacheRepository, cache repository management interface

    +

    Constructors

    Methods

    Constructors

    • Create an instance of CacheRepository

      +

      Parameters

      • client: Redis

        Redis client instance

        +

      Returns CacheRepository

    Methods

    • Return the value of the previous response for the requested path if is present in the cache

      +

      Parameters

      • path: string

        Route path cached

        +
      • uuid: string

        Request identification, for trace propuse

        +

      Returns Promise<null | CacheEntry>

    • Return the value of the previous response for the requested path if is present in the cache

      +

      Parameters

      • path: string

        Route path cached

        +
      • response: CacheEntry

        Response to store in the cache, must be a string

        +
      • uuid: string

        Request identification, for trace propuse

        +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_mongo-provider.Mongo.Port.html b/docs/classes/_mdf.js_mongo-provider.Mongo.Port.html new file mode 100644 index 00000000..da6385d3 --- /dev/null +++ b/docs/classes/_mdf.js_mongo-provider.Mongo.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): MongoClient
    • Return the underlying port instance

      +

      Returns MongoClient

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port, alias to stop

      +

      Returns Promise<void>

    • Start the port, making it available

      +

      Returns Promise<void>

    • Stop the port, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_mqtt-provider.MQTT.Port.html b/docs/classes/_mdf.js_mqtt-provider.MQTT.Port.html new file mode 100644 index 00000000..4ad1dbea --- /dev/null +++ b/docs/classes/_mdf.js_mqtt-provider.MQTT.Port.html @@ -0,0 +1,16 @@ +Port | @mdf.js

    MQTT port implementation

    +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): MqttClient
    • Return the underlying port instance

      +

      Returns MqttClient

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2-core.Accessors.html b/docs/classes/_mdf.js_openc2-core.Accessors.html new file mode 100644 index 00000000..ec8fef71 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core.Accessors.html @@ -0,0 +1,43 @@ +Accessors | @mdf.js

    Constructors

    Methods

    • Return the action of the actual command

      +

      Parameters

      • command: Command

        command to be processed

        +

      Returns Action

      action

      +
    • Return the a property from actuators in the command

      +

      Parameters

      • command: Command

        command to be processed

        +
      • profile: string

        actuator profile to find

        +

      Returns any

      property value

      +
    • Return the actuators in the command

      +

      Parameters

      • command: Command

        command to be processed

        +

      Returns string[]

      actuators

      +
    • Return the actuators in the command message

      +

      Parameters

      Returns string[]

      actuators

      +
    • Return the delay allowed from command

      +

      Parameters

      • command: Command

        message to be processed

        +

      Returns number

      delay in milliseconds

      +
    • Return the delay allowed from command message

      +

      Parameters

      Returns number

      delay in milliseconds

      +
    • Convert consumer status to Subcomponent status

      +

      Parameters

      Returns "pass" | "fail" | "warn"

      Subcomponent status

      +
    • Return the target of the actual command

      +

      Parameters

      • command: Command

        command to be processed

        +

      Returns string

      target

      +
    • Return the target of the actual message

      +

      Parameters

      Returns string

      target

      +
    diff --git a/docs/classes/_mdf.js_openc2-core.Consumer.html b/docs/classes/_mdf.js_openc2-core.Consumer.html new file mode 100644 index 00000000..17f6edbb --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core.Consumer.html @@ -0,0 +1,100 @@ +Consumer | @mdf.js

    A service is a special kind of resource that besides Resource properties, it could offer:

    +
      +
    • Its own REST API endpoints, using an express router, to expose details about service, this +endpoints will be exposed under the observability paths.
    • +
    • A links property to define the endpoints that the service expose, this information will be +exposed in the observability paths.
    • +
    • A metrics property to expose the metrics of the service, this registry will be merged with the +global metrics registry.
    • +
    +

    Hierarchy (View Summary)

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get actuator(): undefined | string[]
    • Consumer actuators

      +

      Returns undefined | string[]

    • set actuator(value: undefined | string[]): void
    • Parameters

      • value: undefined | string[]

      Returns void

    • Return links offered by this service

      +

      Returns Links

    • get name(): string
    • Component name

      +

      Returns string

    • get profiles(): undefined | string[]
    • Consumer profiles

      +

      Returns undefined | string[]

    • set profiles(value: undefined | string[]): void
    • Parameters

      • value: undefined | string[]

      Returns void

    • get router(): Router
    • Return an Express router with access to OpenC2 information

      +

      Returns Router

    • get status(): "pass" | "fail" | "warn"
    • Component health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 component

      +

      Returns Promise<void>

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "error"

        error event

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "command"

        command event

        +

      Returns this

    • Connect the OpenC2 Adapter to the underlayer transport system and perform the startup of the +component

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter to the underlayer transport system and perform the shutdown of +the component

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the command event, emitted when a new command is received

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the command event.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the command event, emitted when a new command is received

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the command event, emitted when a new command is received. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the command event.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_openc2-core.ConsumerMap.html b/docs/classes/_mdf.js_openc2-core.ConsumerMap.html new file mode 100644 index 00000000..f94dffe5 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core.ConsumerMap.html @@ -0,0 +1,49 @@ +ConsumerMap | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • ConsumerMap

    Implements

    Constructors

    • Create a new instance of the consumer map

      +

      Parameters

      • name: string

        name of the producer

        +
      • agingInterval: number

        agingInterval to update the consumer map

        +
      • maxAge: number

        Max allowed age in in milliseconds for a table entry

        +

      Returns ConsumerMap

    Properties

    componentId: string = ...

    Component identification

    +
    name: string

    name of the producer

    +

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Clean the nodes map

      +

      Returns void

    • Fake close method used to implement the Resource interface

      +

      Returns Promise<void>

    • Return the consumers identifiers that has the indicated action/target pair

      +

      Parameters

      • action: string

        action to search

        +
      • target: string

        target requested

        +

      Returns string[]

    • Returns the grouped features of all the consumer in the map

      +

      Returns { pairs: ActionTargetPairs; profiles: string[] }

    • Emitted when a producer's operation has some problem

      +

      Parameters

      • event: "error"
      • listener: (error: Crash | Error) => void

      Returns this

    • Emitted on every state change

      +

      Parameters

      • event: "status"
      • listener: (status: "pass" | "fail" | "warn") => void

      Returns this

    • Emitted when new nodes are included included map

      +

      Parameters

      • event: "new"
      • listener: (nodes: string[]) => void

      Returns this

    • Emitted when some nodes has been aged

      +

      Parameters

      • event: "aged"
      • listener: (nodes: string[]) => void

      Returns this

    • Emitted when nodes are updated

      +

      Parameters

      • event: "update"
      • listener: (nodes: string[]) => void

      Returns this

    • Emitted when the consumer has been updated

      +

      Parameters

      • event: "updated"
      • listener: (nodes: string[]) => void

      Returns this

    • Fake start method used to implement the Resource interface

      +

      Returns Promise<void>

    • Fake stop method used to implement the Resource interface

      +

      Returns Promise<void>

    • Perform the update of the health map

      +

      Parameters

      Returns void

    diff --git a/docs/classes/_mdf.js_openc2-core.Gateway.html b/docs/classes/_mdf.js_openc2-core.Gateway.html new file mode 100644 index 00000000..97b96c81 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core.Gateway.html @@ -0,0 +1,77 @@ +Gateway | @mdf.js

    A service is a special kind of resource that besides Resource properties, it could offer:

    +
      +
    • Its own REST API endpoints, using an express router, to expose details about service, this +endpoints will be exposed under the observability paths.
    • +
    • A links property to define the endpoints that the service expose, this information will be +exposed in the observability paths.
    • +
    • A metrics property to expose the metrics of the service, this registry will be merged with the +global metrics registry.
    • +
    +

    Hierarchy

    • EventEmitter
      • Gateway

    Implements

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    • get router(): Router
    • Return an Express router with access to errors registry

      +

      Returns Router

    • get status(): "pass" | "fail" | "warn"
    • Component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 gateway

      +

      Returns Promise<void>

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "error"

        error event

        +

      Returns this

    • Connect the OpenC2 underlayer component and perform the startup of the component

      +

      Returns Promise<void>

    • Disconnect the OpenC2 underlayer component and perform the startup of the component

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_openc2-core.Producer.html b/docs/classes/_mdf.js_openc2-core.Producer.html new file mode 100644 index 00000000..a1cba66f --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core.Producer.html @@ -0,0 +1,93 @@ +Producer | @mdf.js

    A service is a special kind of resource that besides Resource properties, it could offer:

    +
      +
    • Its own REST API endpoints, using an express router, to expose details about service, this +endpoints will be exposed under the observability paths.
    • +
    • A links property to define the endpoints that the service expose, this information will be +exposed in the observability paths.
    • +
    • A metrics property to expose the metrics of the service, this registry will be merged with the +global metrics registry.
    • +
    +

    Hierarchy (View Summary)

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +
    consumerMap: ConsumerMap

    Consumer Map

    +

    Accessors

    • Return links offered by this service

      +

      Returns Links

    • get name(): string
    • Component name

      +

      Returns string

    • get router(): Router
    • Return an Express router with access to OpenC2 information

      +

      Returns Router

    • get status(): "pass" | "fail" | "warn"
    • Component health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 component

      +

      Returns Promise<void>

    • Issue a new command to the requested consumers. If '' is indicated as a consumer, the command +will be broadcasted. If an actuator is indicated in the command the command will not be +broadcasted even if it include the '' symbol.

      +

      Parameters

      Returns Promise<ResponseMessage[]>

    • Issue a new command to the requested consumers. If '' is indicated as a consumer, the command +will be broadcasted. If an actuator is indicated in the command the command will not be +broadcasted even if it include the '' symbol.

      +

      Parameters

      • to: string[]

        Consumer objetive of this command

        +
      • content: Command

        Command to be issued

        +

      Returns Promise<ResponseMessage[]>

    • Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command +will be broadcasted.

      +

      Parameters

      • to: string[]

        Consumer objetive of this command

        +
      • action: Action

        command action

        +
      • target: Target

        command target

        +

      Returns Promise<ResponseMessage[]>

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "error"

        error event

        +

      Returns this

    • Connect the OpenC2 Adapter to the underlayer transport system and perform the startup of the +component

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter to the underlayer transport system and perform the shutdown of +the component

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the error event, emitted when the component detects an error. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component changes its status. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the error event.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error) => void

        Error event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the status event.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf_js_openc2_core.Registry.html b/docs/classes/_mdf.js_openc2-core.Registry.html similarity index 56% rename from docs/classes/_mdf_js_openc2_core.Registry.html rename to docs/classes/_mdf.js_openc2-core.Registry.html index 33145e6e..c0509582 100644 --- a/docs/classes/_mdf_js_openc2_core.Registry.html +++ b/docs/classes/_mdf.js_openc2-core.Registry.html @@ -1,44 +1,44 @@ -Registry | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +Registry | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, besides the error handling and identity, it has a start, stop and close methods to manage the resource lifecycle. It also has a checks property to define the checks that will be performed over the resource to achieve the resulted status. -The most typical example of a resource are the Provider that allow to access to external +The most typical example of a resource are the Provider that allow to access to external databases, message brokers, etc.

    -

    Hierarchy

    • EventEmitter
      • Registry

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Creates a new Register instance

      +

    Hierarchy

    • EventEmitter
      • Registry

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Creates a new Register instance

      Parameters

      • name: string

        Component name

      • maxInactivityTime: number = Constants.DEFAULT_MAX_INACTIVITY_TIME

        Max time in minutes that a job could be pending state

      • registerLimit: number = Constants.DEFAULT_MESSAGES_REGISTERS_LIMIT

        Maximum number of entries in the message register

        -

      Returns Registry

    Properties

    componentId: string = ...

    Component identification

    -
    executedJobs: CommandJobDone[] = []

    Processed jobs

    -
    messages: Message[] = []

    Array of messages used as fifo register

    +

    Returns Registry

    Properties

    componentId: string = ...

    Component identification

    +
    executedJobs: CommandJobDone[] = []

    Processed jobs

    +
    messages: Message[] = []

    Array of messages used as fifo register

    name: string

    Component name

    -
    pendingJobs: Map<string, CommandJobHandler> = ...

    Pending commands

    -

    Accessors

    • get checks(): Checks<any>
    • Return the status of the stream in a standard format

      -

      Returns Checks<any>

      check object as defined in the draft standard +

    pendingJobs: Map<string, CommandJobHandler> = ...

    Pending commands

    +

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Return the status of the register

      -

      Returns "pass" | "fail" | "warn"

    Methods

    • Perform the cleaning of all the resources

      -

      Returns void

    • Fake close method used to implement the Resource interface

      -

      Returns Promise<void>

    • get status(): "pass" | "fail" | "warn"
    • Return the status of the register

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Perform the cleaning of all the resources

      +

      Returns void

    • Fake close method used to implement the Resource interface

      +

      Returns Promise<void>

    • Add a new job to the registry of the jobs managed by this consumer

      -

      Parameters

      Returns void

    • Add a new message to the registry of the last messages managed by this consumer

      -

      Parameters

      • message: Message

        message to be added

        -

      Returns void

    • Fake start method used to implement the Resource interface

      -

      Returns Promise<void>

    • Fake stop method used to implement the Resource interface

      -

      Returns Promise<void>

    +

    Returns undefined | CommandJobHandler

    • Add a new job to the registry of the jobs managed by this consumer

      +

      Parameters

      Returns void

    • Add a new message to the registry of the last messages managed by this consumer

      +

      Parameters

      • message: Message

        message to be added

        +

      Returns void

    • Fake start method used to implement the Resource interface

      +

      Returns Promise<void>

    • Fake stop method used to implement the Resource interface

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper-1.html b/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper-1.html new file mode 100644 index 00000000..434bd8a1 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper-1.html @@ -0,0 +1,29 @@ +AdapterWrapper | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • AdapterWrapper

    Implements

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get componentId(): string
    • Component identifier

      +

      Returns string

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper.html b/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper.html new file mode 100644 index 00000000..b2a8aa00 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.AdapterWrapper.html @@ -0,0 +1,31 @@ +AdapterWrapper | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • AdapterWrapper

    Implements

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get componentId(): string
    • Component identifier

      +

      Returns string

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Subscribe the incoming message handler to the underlayer transport system

      +

      Parameters

      Returns Promise<void>

    • Unsubscribe the incoming message handler from the underlayer transport system

      +

      Parameters

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.Component.html b/docs/classes/_mdf.js_openc2-core._internal_.Component.html new file mode 100644 index 00000000..3e88b84a --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.Component.html @@ -0,0 +1,43 @@ +Component | @mdf.js

    A service is a special kind of resource that besides Resource properties, it could offer:

    +
      +
    • Its own REST API endpoints, using an express router, to expose details about service, this +endpoints will be exposed under the observability paths.
    • +
    • A links property to define the endpoints that the service expose, this information will be +exposed in the observability paths.
    • +
    • A metrics property to expose the metrics of the service, this registry will be merged with the +global metrics registry.
    • +
    +

    Type Parameters

    • T
    • K

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Events

    on +

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    • get router(): Router
    • Return an Express router with access to OpenC2 information

      +

      Returns Router

    • get status(): "pass" | "fail" | "warn"
    • Component health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 component

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system and perform the startup of the +component

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter to the underlayer transport system and perform the shutdown of +the component

      +

      Returns Promise<void>

    Events

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Multi | Error) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component status changes.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.Controller.html b/docs/classes/_mdf.js_openc2-core._internal_.Controller.html new file mode 100644 index 00000000..211ff1dd --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.Controller.html @@ -0,0 +1,18 @@ +Controller | @mdf.js

    Controller class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Execute a command over the producer or consumer

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

        +
      • response message
      • +
      +
    • Return array of messages, pendingJobs and jobs used as fifo registry

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.HealthWrapper.html b/docs/classes/_mdf.js_openc2-core._internal_.HealthWrapper.html new file mode 100644 index 00000000..54a0f1ce --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.HealthWrapper.html @@ -0,0 +1,32 @@ +HealthWrapper | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • HealthWrapper

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Regular OpenC2 consumer implementation. This class allows the management of incoming command +and the underlayer Adapter. The main task of this class is to filter incoming commands that are +not related with the instance or are not supported

      +

      Parameters

      • name: string

        Component name used as node identifier for OpenC2

        +
      • components: Resource[]

        Health components to be monitored

        +

      Returns HealthWrapper

    Properties

    componentId: string = ...

    Component identification

    +
    name: string

    Component name used as node identifier for OpenC2

    +

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Add a new component to health wrapper

      +

      Parameters

      • component: Resource

        component to be added to the health wrapper

        +

      Returns void

    • Fake close method used to implement the Resource interface

      +

      Returns Promise<void>

    • Fake start method used to implement the Resource interface

      +

      Returns Promise<void>

    • Fake stop method used to implement the Resource interface

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.Model.html b/docs/classes/_mdf.js_openc2-core._internal_.Model.html new file mode 100644 index 00000000..03d05be2 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.Model.html @@ -0,0 +1,11 @@ +Model | @mdf.js

    Model class

    +

    Constructors

    Methods

    Constructors

    • Create an instance of model class

      +

      Parameters

      Returns Model

    Methods

    • Return array of messages used as fifo registry

      +

      Returns Promise<Message[]>

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.Router.html b/docs/classes/_mdf.js_openc2-core._internal_.Router.html new file mode 100644 index 00000000..c5cdedef --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.Router.html @@ -0,0 +1,9 @@ +Router | @mdf.js

    Router class

    +

    Hierarchy

    • EventEmitter
      • Router

    Constructors

    Properties

    Methods

    on +

    Constructors

    • Create a new instance of the Router class

      +

      Parameters

      • register: Registry

        Register used by this component

        +
      • path: string = PREFIX_PATH

        prefix path for all the routes

        +

      Returns Router

    Properties

    router: Router

    Methods

    diff --git a/docs/classes/_mdf.js_openc2-core._internal_.Service.html b/docs/classes/_mdf.js_openc2-core._internal_.Service.html new file mode 100644 index 00000000..8bcd1f80 --- /dev/null +++ b/docs/classes/_mdf.js_openc2-core._internal_.Service.html @@ -0,0 +1,19 @@ +Service | @mdf.js

    Service class

    +

    Hierarchy

    • EventEmitter
      • Service

    Constructors

    Methods

    Constructors

    Methods

    • Return array of jobs used as fifo registry

      +

      Returns Promise<Result<"command">[]>

    • Return array of messages used as fifo registry

      +

      Returns Promise<Message[]>

    • Return array of pendingJobs used as fifo registry

      +

      Returns Promise<Result<"command">[]>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyConsumerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyConsumerAdapter.html new file mode 100644 index 00000000..0448a653 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyConsumerAdapter.html @@ -0,0 +1,30 @@ +DummyConsumerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter from the underlayer transport system

      +

      Returns Promise<void>

    • Subscribe the incoming message handler to the underlayer transport system

      +

      Parameters

      • handler: any

        handler to be used

        +

      Returns Promise<void>

    • Unsubscribe the incoming message handler from the underlayer transport system

      +

      Parameters

      • handler: any

        handler to be used

        +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyProducerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyProducerAdapter.html new file mode 100644 index 00000000..c258d33f --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.Dummy.DummyProducerAdapter.html @@ -0,0 +1,27 @@ +DummyProducerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter from the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisConsumerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisConsumerAdapter.html new file mode 100644 index 00000000..b15103de --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisConsumerAdapter.html @@ -0,0 +1,31 @@ +RedisConsumerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisProducerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisProducerAdapter.html new file mode 100644 index 00000000..c70e2f16 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.Redis.RedisProducerAdapter.html @@ -0,0 +1,28 @@ +RedisProducerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html new file mode 100644 index 00000000..3d2580e0 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html @@ -0,0 +1,31 @@ +SocketIOConsumerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html b/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html new file mode 100644 index 00000000..328fdbc0 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html @@ -0,0 +1,28 @@ +SocketIOProducerAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get checks(): Checks<any>
    • Component checks

      +

      Returns Checks<any>

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2.Factory.Consumer.html b/docs/classes/_mdf.js_openc2.Factory.Consumer.html new file mode 100644 index 00000000..f1abb5bc --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Factory.Consumer.html @@ -0,0 +1,13 @@ +Consumer | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_openc2.Factory.GatewayFactory.html b/docs/classes/_mdf.js_openc2.Factory.GatewayFactory.html new file mode 100644 index 00000000..60f5709b --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Factory.GatewayFactory.html @@ -0,0 +1,12 @@ +GatewayFactory | @mdf.js

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_openc2.Factory.Producer.html b/docs/classes/_mdf.js_openc2.Factory.Producer.html new file mode 100644 index 00000000..3804e147 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.Factory.Producer.html @@ -0,0 +1,13 @@ +Producer | @mdf.js

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_openc2.ServiceBus.html b/docs/classes/_mdf.js_openc2.ServiceBus.html new file mode 100644 index 00000000..4e8e7343 --- /dev/null +++ b/docs/classes/_mdf.js_openc2.ServiceBus.html @@ -0,0 +1,31 @@ +ServiceBus | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy

    • EventEmitter
      • ServiceBus

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +
    name: string

    name of the service bus

    +

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Return the status of the server

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the server and disconnect all the actual connections

      +

      Returns Promise<void>

    • Emitted when a server operation has some problem

      +

      Parameters

      • event: "error"
      • listener: (error: Crash | Error) => void

      Returns this

    • Emitted on every state change

      +

      Parameters

      • event: "status"
      • listener: (status: "pass" | "fail" | "warn") => void

      Returns this

    • Start the underlayer Socket.IO server

      +

      Returns Promise<void>

    • Close the server and disconnect all the actual connections

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2._internal_.Adapter.html b/docs/classes/_mdf.js_openc2._internal_.Adapter.html new file mode 100644 index 00000000..0cbca6fb --- /dev/null +++ b/docs/classes/_mdf.js_openc2._internal_.Adapter.html @@ -0,0 +1,9 @@ +Adapter | @mdf.js

    Hierarchy (View Summary)

    Constructors

    Properties

    Accessors

    Constructors

    • Create a new OpenC2 adapter for Redis

      +

      Parameters

      • adapterOptions: AdapterOptions

        Adapter configuration options

        +
      • type: "producer" | "consumer"

        component type

        +

      Returns Adapter

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    diff --git a/docs/classes/_mdf.js_openc2._internal_.AddressMapper.html b/docs/classes/_mdf.js_openc2._internal_.AddressMapper.html new file mode 100644 index 00000000..121f3245 --- /dev/null +++ b/docs/classes/_mdf.js_openc2._internal_.AddressMapper.html @@ -0,0 +1,26 @@ +AddressMapper | @mdf.js

    Constructors

    Methods

    • Delete an entry from the address map

      +

      Parameters

      Returns void

    • Get the Socket.IO id from the OpenC2 id

      +

      Parameters

      • openC2Id: string

        OpenC2 id to be mapped

        +

      Returns undefined | string

      Socket.IO id

      +

      if no valid OpenC2 id is provided

      +
      const mapper = new AddressMapper();
      mapper.update('CmFw2HksBDH5Q6VMAAAC', 'myId');
      mapper.update('mK8mSuq2R0QxLDdHAAAF', 'myOtherId');
      mapper.getByOpenC2Id('otherId'); // undefined
      mapper.getByOpenC2Id('CmFw2HksBDH5Q6VMAAAC'); // 'myId'
      mapper.getByOpenC2Id('mK8mSuq2R0QxLDdHAAAF'); // 'myOtherId' +
      + +
    • Get the OpenC2 id that match with the provided Socket.IO id

      +

      Parameters

      • socketId: string

        socket identification to be mapped

        +

      Returns undefined | string

      OpenC2 id

      +

      if no valid URL is provided URL

      +
      const mapper = new AddressMapper();
      mapper.update('CmFw2HksBDH5Q6VMAAAC', 'myId');
      mapper.update('mK8mSuq2R0QxLDdHAAAF', 'myOtherId');
      mapper.getBySocketId('otherId'); // undefined
      mapper.getBySocketId('CmFw2HksBDH5Q6VMAAAC'); // 'myId'
      mapper.getBySocketId('mK8mSuq2R0QxLDdHAAAF'); // 'myOtherId' +
      + +
    • Update or create a new address entry in the map

      +

      Parameters

      • socketId: string

        Socket.IO to be mapped

        +
      • openC2Id: string

        OpenC2 identification to be mapped

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_openc2._internal_.DummyAdapter.html b/docs/classes/_mdf.js_openc2._internal_.DummyAdapter.html new file mode 100644 index 00000000..4b26bb57 --- /dev/null +++ b/docs/classes/_mdf.js_openc2._internal_.DummyAdapter.html @@ -0,0 +1,25 @@ +DummyAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Disconnect the OpenC2 Adapter from the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2._internal_.RedisAdapter.html b/docs/classes/_mdf.js_openc2._internal_.RedisAdapter.html new file mode 100644 index 00000000..d7953c47 --- /dev/null +++ b/docs/classes/_mdf.js_openc2._internal_.RedisAdapter.html @@ -0,0 +1,26 @@ +RedisAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Disconnect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_openc2._internal_.SocketIOAdapter.html b/docs/classes/_mdf.js_openc2._internal_.SocketIOAdapter.html new file mode 100644 index 00000000..2815b482 --- /dev/null +++ b/docs/classes/_mdf.js_openc2._internal_.SocketIOAdapter.html @@ -0,0 +1,26 @@ +SocketIOAdapter | @mdf.js

    A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

    +

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    Properties

    componentId: string = ...

    Component identification

    +

    Accessors

    • get name(): string
    • Component name

      +

      Returns string

    • get status(): "pass" | "fail" | "warn"
    • Adapter health status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Close the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    • Connect the OpenC2 Adapter to the underlayer transport system

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_redis-provider.Redis.Port.html b/docs/classes/_mdf.js_redis-provider.Redis.Port.html new file mode 100644 index 00000000..bd8d53ed --- /dev/null +++ b/docs/classes/_mdf.js_redis-provider.Redis.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Redis
    • Return the underlying port instance

      +

      Returns Redis

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port, alias to stop

      +

      Returns Promise<void>

    • Start the port, making it available

      +

      Returns Promise<void>

    • Stop the port, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_s3-provider.S3.Port.html b/docs/classes/_mdf.js_s3-provider.S3.Port.html new file mode 100644 index 00000000..b310d81c --- /dev/null +++ b/docs/classes/_mdf.js_s3-provider.S3.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    • Implementation of functionalities of a S3 port instance.

      +

      Parameters

      • config: S3ClientConfig

        Port configuration options

        +
      • logger: LoggerInstance

        Port logger, to be used internally

        +

      Returns S3.Port

    Accessors

    • get client(): S3Client
    • Return the underlying port instance

      +

      Returns S3Client

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port, alias to stop

      +

      Returns Promise<void>

    • Start the port, making it available

      +

      Returns Promise<void>

    • Stop the port, making it unavailable

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry.ServiceRegistry.html b/docs/classes/_mdf.js_service-registry.ServiceRegistry.html new file mode 100644 index 00000000..9649c8ec --- /dev/null +++ b/docs/classes/_mdf.js_service-registry.ServiceRegistry.html @@ -0,0 +1,64 @@ +ServiceRegistry | @mdf.js

    Class ServiceRegistry<CustomSettings>

    Type Parameters

    Hierarchy

    • EventEmitter
      • ServiceRegistry

    Constructors

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      Service Register status

      +

    Methods

    • Gets the value at path of object. If the resolved value is undefined, the defaultValue is +returned in its place.

      +

      Type Parameters

      • T

        Type of the property to return

        +

      Parameters

      • path: string | string[]

        path to the property to get

        +
      • OptionaldefaultValue: T

        default value to return if the property is not found

        +

      Returns undefined | T

    • Gets the value at path of object. If the resolved value is undefined, the defaultValue is +returned in its place.

      +

      Type Parameters

      • P extends string | number | symbol

      Parameters

      • key: P

        path to the property to get

        +
      • OptionaldefaultValue: CustomSettings[P]

        default value to return if the property is not found

        +

      Returns undefined | CustomSettings[P]

    • Register a resource within the service observability

      +

      Parameters

      Returns void

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "command"

        command event

        +

      Returns this

    • Perform the initialization of all the service resources that has been attached

      +

      Returns Promise<void>

    • Perform the stop of all the service resources that has been attached

      +

      Returns Promise<void>

    Events

    • Add a listener for the command event, emitted when a new command is received

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the command event.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Add a listener for the command event, emitted when a new command is received

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Add a listener for the command event, emitted when a new command is received. This is a +one-time event, the listener will be removed after the first emission.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    • Removes the specified listener from the listener array for the command event.

      +

      Parameters

      • event: "command"

        command event

        +
      • listener: (job: CommandJobHandler) => void

        Command event listener

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Aggregator-1.html b/docs/classes/_mdf.js_service-registry._internal_.Aggregator-1.html new file mode 100644 index 00000000..e2ff8332 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Aggregator-1.html @@ -0,0 +1,27 @@ +Aggregator | @mdf.js

    MetricsAggregator class manages all the metrics for this artifact, +integrating with Prometheus for metrics collection and aggregation.

    +

    Constructors

    • Create a new instance of MetricsAggregator

      +

      Parameters

      • logger: LoggerInstance

        Instance for logging

        +
      • Optionalport: AggregatorRegistry<"text/plain; version=0.0.4; charset=utf-8">

        Optional AggregatorRegistry for cluster metrics

        +

      Returns Aggregator

    Accessors

    • get registry(): Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Return the registry used by the aggregator

      +

      Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

    Methods

    • Clear the registry

      +

      Returns void

    • Retrieves a single metric by name from the registry.

      +

      Parameters

      • name: string

        The name of the metric to retrieve

        +

      Returns undefined | Metric

    • Retrieves a single metric value in JSON format by name.

      +

      Parameters

      • name: string

        The name of the metric to retrieve

        +

      Returns Promise<undefined | MetricObjectWithValues<MetricValue<string>>>

    • Retrieves a single metric value in Prometheus format by name.

      +

      Parameters

      • name: string

        The name of the metric to retrieve

        +

      Returns Promise<string>

    • Return the actual metrics in JSON format

      +

      Returns Promise<Response>

    • Return the metrics in text/plain format

      +

      Returns Promise<Response>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Aggregator-2.html b/docs/classes/_mdf.js_service-registry._internal_.Aggregator-2.html new file mode 100644 index 00000000..f5d877c4 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Aggregator-2.html @@ -0,0 +1,58 @@ +Aggregator | @mdf.js

    The Aggregator class serves as a central point for collecting and aggregating health checks +and statuses from various components within an application. It also allows for the integration +of external and worker-specific checks to provide a comprehensive view of the application's +health status.

    +

    This class extends EventEmitter to emit status updates, enabling other parts of the application +to react to changes in health status as necessary.

    +

    Hierarchy

    • EventEmitter
      • Aggregator

    Constructors

    • Create an instance of the Health aggregator

      +

      Parameters

      • metadata: Metadata

        Metadata describing the application, used to enrich the health data.

        +
      • logger: LoggerInstance

        Logger instance for logging activities related to health monitoring.

        +

      Returns Aggregator

    Properties

    output: undefined | string = undefined

    Public output

    +

    Accessors

    • get checks(): Checks<any>
    • Aggregates checks from all sources: registered components, external checks, and worker checks.

      +

      Returns Checks<any>

    • get health(): Layer.App.Health
    • Computes the health status of the application by aggregating individual component checks +and determining the overall status.

      +

      Returns Layer.App.Health

    • get status(): "pass" | "fail" | "warn"
    • Overall component status

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Update or add a check measure. +This should be used to inform about the state of resources behind the Component/Microservice, +for example states of connections with field devices.

      +

      The new check will be taking into account in the overall health status. +The new check will be included in the checks object with the key "component:measure". +If this key already exists, the componentId of the check parameter will be checked, if +there is a check with the same componentId in the array, the check will be updated, in other +case the new check will be added to the existing array.

      +

      The maximum number external checks entries is 10, and the maximum number of checks per entry +is 100.

      +

      Parameters

      • component: string

        component identification

        +
      • measure: string

        measure identification

        +
      • check: Check<any>

        check to be updated or included

        +

      Returns boolean

      true, if the check has been updated or included

      +
    • Adds a timestamped note to the health status.

      +

      Parameters

      • note: string

        Note to be added.

        +

      Returns void

    • Update or add a check measure for a worker. +This should be used to inform about the state of resources behind the worker. +The new check will be taking into account in the overall health status. +The new check will be included in the checks object with the key "component:measure". +If this key already exists, the componentId of the check parameter will be checked, if +there is a check with the same componentId in the array, the check will be updated, in other +case the new check will be added to the existing array.

      +

      Parameters

      • component: string

        component identification

        +
      • measure: string

        measure identification

        +
      • check: Check<any>

        check to be updated or included

        +

      Returns boolean

      true, if the check has been updated or included

      +
    • Close the aggregator

      +

      Returns void

    • Register a resource or a list of resources to monitor for errors.

      +

      Parameters

      • component: Resource | Resource[]

        Resource or list of resources to be registered

        +

      Returns void

    • Update the health checks associated with workers.

      +

      Parameters

      • checks: Checks<any>

        Checks to be updated or included

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Aggregator.html b/docs/classes/_mdf.js_service-registry._internal_.Aggregator.html new file mode 100644 index 00000000..9d96cfa1 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Aggregator.html @@ -0,0 +1,33 @@ +Aggregator | @mdf.js

    The Aggregator class is responsible for aggregating and managing error events +from various components within an application. It allows for centralized error +handling, supporting a structured approach to error logging and potentially +error recovery strategies.

    +

    It extends EventEmitter to emit error events, enabling other parts of the +application to listen and respond to these error events as needed.

    +

    Hierarchy

    • EventEmitter
      • Aggregator

    Constructors

    • Creates an instance of Aggregator.

      +

      Parameters

      • logger: LoggerInstance

        Logger instance for logging error registration and handling.

        +
      • maxSize: number = DEFAULT_CONFIG_REGISTER_MAX_LIST_SIZE

        Maximum number of errors to keep in the registry. Older errors are removed as +new ones are added.

        +
      • includeStack: boolean = DEFAULT_CONFIG_REGISTER_INCLUDE_STACK

        Flag to determine if stack traces should be included in the error +records.

        +

      Returns Aggregator

    Accessors

    • get lastUpdate(): string
    • Returns string

      Last update date

      +
    • get size(): number
    • Returns number

      The current number of registered errors

      +

    Methods

    • Clear the error registry

      +

      Returns void

    • Cleans up by removing error event listeners and clearing the registry.

      +

      Returns void

    • Adds an error to the registry, converting it to a structured format.

      +

      Parameters

      Returns void

    • Register a component or a list of components to monitor for errors.

      +

      Parameters

      Returns void

    • Updates the registry with errors from worker threads/processes.

      +

      Parameters

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.ControlManager.html b/docs/classes/_mdf.js_service-registry._internal_.ControlManager.html new file mode 100644 index 00000000..c4dfb1d3 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.ControlManager.html @@ -0,0 +1,29 @@ +ControlManager | @mdf.js

    ControlManager handles OpenC2 command and control interactions, serving as the bridge between +OpenC2 Consumers and the Service. +It extends EventEmitter to re-emit the events of the OpenC2 Consumer (e.g., command execution, +errors, and status updates).

    +

    Hierarchy

    • EventEmitter
      • ControlManager

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Constructor for the ControlManager class.

      +

      Parameters

      • serviceRegistrySettings: ServiceRegistryOptions<Record<string, any>>

        Service Registry settings, which include the consumer and +adapter configurations.

        +
      • logger: LoggerInstance

        Logger instance.

        +
      • OptionaldefaultResolver: ResolverMap

        This is the default resolver map for the OpenC2 interface, which is +merged with the resolver map from the service registry settings, if provided. The default +value, passed from the Service Registry instance include resolvers for the features:

        +
          +
        • query: health, stats, errors and config
        • +
        • start: resources
        • +
        • stop: resources
        • +
        +

      Returns ControlManager

    Properties

    instance?: Consumer

    OpenC2 Consumer instance

    +

    Accessors

    • get error(): undefined | Multi
    • Returns the validation error, if exist.

      +

      Returns undefined | Multi

      Multi error, if exist.

      +

    Methods

    • Starts the OpenC2 Consumer instance.

      +

      Returns Promise<void>

      Promise

      +
    • Stops the OpenC2 Consumer instance.

      +

      Returns Promise<void>

      Promise

      +
    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Controller-1.html b/docs/classes/_mdf.js_service-registry._internal_.Controller-1.html new file mode 100644 index 00000000..a9ef031c --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Controller-1.html @@ -0,0 +1,10 @@ +Controller | @mdf.js

    Controller class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Get all the error in the registry

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Controller-2.html b/docs/classes/_mdf.js_service-registry._internal_.Controller-2.html new file mode 100644 index 00000000..9a4ea948 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Controller-2.html @@ -0,0 +1,11 @@ +Controller | @mdf.js

    Controller class

    +

    Constructors

    Methods

    Constructors

    • Create an instance of Controller class

      +

      Parameters

      • service: Service

        service instance

        +
      • isCluster: boolean

        indicates that the instance of this metrics service is running in a cluster

        +

      Returns Controller

    Methods

    • Return all the actual metrics of this artifact

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Controller-3.html b/docs/classes/_mdf.js_service-registry._internal_.Controller-3.html new file mode 100644 index 00000000..62f9904c --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Controller-3.html @@ -0,0 +1,10 @@ +Controller | @mdf.js

    Controller class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return the state of all the providers

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Controller.html b/docs/classes/_mdf.js_service-registry._internal_.Controller.html new file mode 100644 index 00000000..7ee56df4 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Controller.html @@ -0,0 +1,15 @@ +Controller | @mdf.js

    Controller class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return the configuration objects

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    • Return the readme object

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.HealthFacade.html b/docs/classes/_mdf.js_service-registry._internal_.HealthFacade.html new file mode 100644 index 00000000..cfb43f6d --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.HealthFacade.html @@ -0,0 +1,55 @@ +HealthFacade | @mdf.js

    The HealthFacade class serves as a comprehensive solution for monitoring and exposing the health +of all components within an application. It abstracts the complexity of health information +aggregation and distribution, making it accessible through a REST API and manageable across +different operational contexts (e.g., standalone, clustered master, and worker processes).

    +

    This class leverages:

    +
      +
    • Aggregator: To aggregate health checks and status events from components.
    • +
    • Port: To handle health information requests and responses in a cluster, accommodating both +master and worker roles.
    • +
    • Router: To expose aggregated health information via a REST API.
    • +
    +

    Hierarchy

    • EventEmitter
      • HealthFacade

    Implements

    Constructors

    Accessors

    • get componentId(): string
    • Returns string

      The application identifier

      +
    • Returns { [link: string]: string }

      Links offered by this service

      +
    • get name(): string
    • Returns string

      The application name

      +
    • get router(): Router
    • Returns Router

      An Express router with access to health information

      +
    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      The health status of the component

      +

    Methods

    • Update or add a check measure. +This should be used to inform about the state of resources behind the Component/Microservice, +for example states of connections with field devices.

      +

      The new check will be taking into account in the overall health status. +The new check will be included in the checks object with the key "component:measure". +If this key already exists, the componentId of the check parameter will be checked, if +there is a check with the same componentId in the array, the check will be updated, in other +case the new check will be added to the existing array.

      +

      The maximum number external checks is 100

      +

      Parameters

      • component: string

        component identification

        +
      • measure: string

        measure identification

        +
      • check: Check<any>

        check to be updated or included

        +

      Returns boolean

      true, if the check has been updated

      +
    • Adds a timestamped note to the health status.

      +

      Parameters

      • note: string

        Note to be added.

        +

      Returns void

    • Close health service

      +

      Returns Promise<void>

    • Register a resource or a list of resources to monitor for errors.

      +

      Parameters

      • component: Resource | Resource[]

        Resource or list of resources to be registered

        +

      Returns void

    • Start health service

      +

      Returns Promise<void>

    • Stop health service

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.MetricsFacade.html b/docs/classes/_mdf.js_service-registry._internal_.MetricsFacade.html new file mode 100644 index 00000000..9ba9ee7f --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.MetricsFacade.html @@ -0,0 +1,57 @@ +MetricsFacade | @mdf.js

    MetricsFacade class serves as a facade to simplify metrics management across all services +in an application. It leverages the prom-client library for metrics management and the express +library to expose these metrics via an HTTP endpoint.

    +

    It accommodates working in cluster environments by optionally creating a new AggregatorRegistry +to hold metrics from all worker nodes, in addition to a separate registry for +application-specific metrics and the default prom-client registry. Thus, it can manage up to +three different registries:

    +
      +
    1. Default prom-client registry for default metrics.
    2. +
    3. Application-specific metrics registry.
    4. +
    5. Cluster-wide metrics registry for environments running in a cluster.
    6. +
    +

    Depending on the environment and the node type (primary or worker), it responds to metric +requests by merging and presenting metrics from appropriate registries.

    +

    Hierarchy

    • EventEmitter
      • MetricsFacade

    Implements

    Constructors

    Accessors

    • get componentId(): string
    • Returns string

      The application identifier

      +
    • Returns { [link: string]: string }

      Links offered by this service

      +
    • get name(): string
    • Returns string

      The application name

      +
    • get registry(): Registry<"text/plain; version=0.0.4; charset=utf-8">
    • Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

      The registry used by the aggregator

      +
    • get router(): Router
    • Returns Router

      An Express router with access to metrics information

      +
    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      The health status of the component

      +

    Methods

    • Clears all metrics from the registry.

      +

      Returns Promise<void>

    • Retrieves a single metric by name.

      +

      Parameters

      • name: string

        The name of the metric.

        +

      Returns undefined | Metric

      The metric or undefined if not found.

      +
    • Retrieves a single metric value in JSON format.

      +

      Parameters

      • name: string

        The name of the metric.

        +

      Returns Promise<undefined | MetricObjectWithValues<MetricValue<string>>>

      A promise resolved with the metric object or undefined if not found.

      +
    • Retrieves a single metric value in Prometheus format.

      +

      Parameters

      • name: string

        The name of the metric.

        +

      Returns Promise<string>

      A promise resolved with the metric value as a string.

      +
    • Returns Promise<Response>

      Metrics in JSON format

      +
    • Returns Promise<Response>

      Metrics in text/plain format

      +
    • Placeholder for starting the metrics service.

      +

      Returns Promise<void>

    • Placeholder for stopping the metrics service.

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Model-1.html b/docs/classes/_mdf.js_service-registry._internal_.Model-1.html new file mode 100644 index 00000000..47aecd27 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Model-1.html @@ -0,0 +1,7 @@ +Model | @mdf.js

    Model class

    +

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Model-2.html b/docs/classes/_mdf.js_service-registry._internal_.Model-2.html new file mode 100644 index 00000000..50c169e2 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Model-2.html @@ -0,0 +1,7 @@ +Model | @mdf.js

    Model class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return all the actual metrics of this artifact

      +

      Parameters

      • jsonFormat: boolean

      Returns Promise<Response>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Model-3.html b/docs/classes/_mdf.js_service-registry._internal_.Model-3.html new file mode 100644 index 00000000..0ce0e487 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Model-3.html @@ -0,0 +1,7 @@ +Model | @mdf.js

    Model class

    +

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Model.html b/docs/classes/_mdf.js_service-registry._internal_.Model.html new file mode 100644 index 00000000..0185c978 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Model.html @@ -0,0 +1,11 @@ +Model | @mdf.js

    Model class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return the readme object

      +

      Returns Promise<undefined | string>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Observability.html b/docs/classes/_mdf.js_service-registry._internal_.Observability.html new file mode 100644 index 00000000..3f32c86b --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Observability.html @@ -0,0 +1,57 @@ +Observability | @mdf.js

    Represents a comprehensive observability service that aggregates various registries +including health checks, metrics, and error logging. This class is responsible for +managing and initializing these registries, attaching services to them, and integrating +them into a unified observability application.

    +

    The service leverages an ObservabilityAppManager to orchestrate the express application +that serves observability endpoints. It allows for dynamic registration of services +to enable monitoring, health checks, and error tracking.

    +

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Initializes the observability service with specified options, setting up +health, metrics, and error registries based on those options.

      +

      Parameters

      • options: ObservabilityOptions

        Configuration options for observability, including settings +for health checks, metrics collection, and error logging.

        +

      Returns Observability

    Properties

    Configuration options for observability, including settings +for health checks, metrics collection, and error logging.

    +

    Accessors

    • Returns Links

      The observability rest api access end points

      +
    • get metrics(): Promise<Response>
    • Returns Promise<Response>

      The metrics of the monitored application

      +
    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      The status of the monitored application

      +

    Methods

    • Update or add a check measure. +This should be used to inform about the state of resources behind the Component/Microservice, +for example states of connections with field devices.

      +

      The new check will be taking into account in the overall health status. +The new check will be included in the checks object with the key "component:measure". +If this key already exists, the componentId of the check parameter will be checked, if +there is a check with the same componentId in the array, the check will be updated, in other +case the new check will be added to the existing array.

      +

      The maximum number external checks is 100

      +

      Parameters

      • component: string

        component identification

        +
      • measure: string

        measure identification

        +
      • check: Check<any>

        check to be updated or included

        +

      Returns boolean

      true, if the check has been updated

      +
    • Adds a timestamped note to the health status.

      +

      Parameters

      • note: string

        Note to be added.

        +

      Returns void

    • Attaches a new service to be monitored under the observability framework. +Services are components of your application that you wish to monitor for +health, track errors for, and collect metrics on.

      +

      Parameters

      • observable: Observable

        The service to attach to observability.

        +

      Returns void

    • Close the observability service

      +

      Returns Promise<void>

    • Adds an error to the registry, converting it to a structured format.

      +

      Parameters

      • error: Crash | Multi | Error

        The error to register.

        +

      Returns void

    • Start the observability service

      +

      Returns Promise<void>

    • Stop the observability service

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.ObservabilityAppManager.html b/docs/classes/_mdf.js_service-registry._internal_.ObservabilityAppManager.html new file mode 100644 index 00000000..15e58644 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.ObservabilityAppManager.html @@ -0,0 +1,22 @@ +ObservabilityAppManager | @mdf.js

    Manages the lifecycle and configuration of an Express application dedicated to observability. +This includes setting up middleware, routing, and server configuration. +It also supports dynamic registration of services and links for enhanced observability.

    +

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get isBuild(): boolean
    • Indicates whether the server has been initialized.

      +

      Returns boolean

    • Returns Links

      The links offered by this service

      +

    Methods

    • Constructs the server with the configured options.

      +

      Returns void

    • Starts the server if it has been built.

      +

      Returns Promise<void>

    • Stops the server if it is running.

      +

      Returns Promise<void>

    • Resets the server to its initial state.

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Port-1.html b/docs/classes/_mdf.js_service-registry._internal_.Port-1.html new file mode 100644 index 00000000..02dd0c3b --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Port-1.html @@ -0,0 +1,10 @@ +Port | @mdf.js

    The Port class is responsible for starting and stopping the cluster communication +and clearing the actual health registries in the master and worker processes.

    +

    Constructors

    Methods

    Constructors

    • Create a new instance of the Port class.

      +

      Parameters

      • logger: LoggerInstance

        Logger instance for logging error registration and handling.

        +

      Returns Port

    Methods

    • Start cluster communication

      +

      Returns void

    • Stop cluster communication

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Port.html b/docs/classes/_mdf.js_service-registry._internal_.Port.html new file mode 100644 index 00000000..bebcc514 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Port.html @@ -0,0 +1,12 @@ +Port | @mdf.js

    The Port class is responsible for starting and stopping the cluster communication +and clearing the actual error registries in the master and worker processes.

    +

    Constructors

    Methods

    Constructors

    • Create a new instance of the Port class.

      +

      Parameters

      • logger: LoggerInstance

        Logger instance for logging error registration and handling.

        +

      Returns Port

    Methods

    • Clear all the actual error registries

      +

      Returns void

    • Start cluster communication

      +

      Returns void

    • Stop cluster communication

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.RegisterFacade.html b/docs/classes/_mdf.js_service-registry._internal_.RegisterFacade.html new file mode 100644 index 00000000..4431f94d --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.RegisterFacade.html @@ -0,0 +1,48 @@ +RegisterFacade | @mdf.js

    The RegisterFacade class provides a centralized solution for error monitoring across all +components of an application. It acts as a facade over various underlying mechanisms to +facilitate error aggregation, error information exposure through REST APIs, and error registry +management.

    +

    It integrates with:

    +
      +
    • Port: To handle inter-process communication for error information in clustered environments, +distinguishing between master and worker processes.
    • +
    • Aggregator: To aggregate error information from different components of the application.
    • +
    +

    This class also provides a REST API endpoint for accessing collected error information and +supports operations for registering errors and clearing the error registry.

    +

    Hierarchy

    • EventEmitter
      • RegisterFacade

    Implements

    Constructors

    Accessors

    • get componentId(): string
    • Returns string

      The application identifier

      +
    • get lastUpdate(): string
    • Returns string

      Last update date

      +
    • Returns { [link: string]: string }

      Links offered by this service

      +
    • get name(): string
    • Returns string

      The application name

      +
    • get router(): Router
    • Returns Router

      An Express router with access to registered errors

      +
    • get size(): number
    • Returns number

      The current number of registered errors

      +
    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      The health status of the component

      +

    Methods

    • Clear the error registry

      +

      Returns void

    • Closes the error registry service, performing cleanup actions as necessary.

      +

      Returns Promise<void>

    • Adds an error to the registry, converting it to a structured format.

      +

      Parameters

      Returns void

    • Registers one or multiple components to be monitored.

      +

      Parameters

      Returns void

    • Starts the error registry service, including the communication port and error event listeners.

      +

      Returns Promise<void>

    • Stops the error registry service, including halting communication and removing event listeners.

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Router-1.html b/docs/classes/_mdf.js_service-registry._internal_.Router-1.html new file mode 100644 index 00000000..ac679c29 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Router-1.html @@ -0,0 +1,8 @@ +Router | @mdf.js

    Router class

    +

    Constructors

    Accessors

    Constructors

    • Create a new instance of the Router class

      +

      Parameters

      • aggregator: Aggregator

        Aggregator used by this component

        +
      • path: string = PREFIX_PATH

        prefix path for all the routes

        +

      Returns Router

    Accessors

    • get router(): Router
    • Express router for health REST API component

      +

      Returns Router

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Router-2.html b/docs/classes/_mdf.js_service-registry._internal_.Router-2.html new file mode 100644 index 00000000..bec02cd2 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Router-2.html @@ -0,0 +1,9 @@ +Router | @mdf.js

    Router class

    +

    Constructors

    Accessors

    Constructors

    • Create a new instance of the Router class

      +

      Parameters

      • aggregator: Aggregator

        Aggregator used by this component

        +
      • isCluster: boolean = false

        indicates that the instance of this metrics service is running in a cluster

        +
      • path: string = PREFIX_PATH

        prefix path for all the routes

        +

      Returns Router

    Accessors

    • get router(): Router
    • Express router for health REST API component

      +

      Returns Router

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Router-3.html b/docs/classes/_mdf.js_service-registry._internal_.Router-3.html new file mode 100644 index 00000000..96ab1b48 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Router-3.html @@ -0,0 +1,8 @@ +Router | @mdf.js

    Router class

    +

    Constructors

    Accessors

    Constructors

    • Create a new instance of the Router class

      +

      Parameters

      • aggregator: Aggregator

        Aggregator used by this component

        +
      • path: string = PREFIX_PATH

        prefix path for all the routes

        +

      Returns Router

    Accessors

    • get router(): Router
    • Express router for health REST API component

      +

      Returns Router

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Router.html b/docs/classes/_mdf.js_service-registry._internal_.Router.html new file mode 100644 index 00000000..d6cc5b7d --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Router.html @@ -0,0 +1,8 @@ +Router | @mdf.js

    Router class

    +

    Constructors

    Accessors

    Constructors

    • Create a new instance of the Router class

      +

      Parameters

      • manager: SettingsManagerAccessors<Record<string, any>>

        Registry used by this component

        +
      • path: string = PREFIX_PATH

        prefix path for all the routes

        +

      Returns Router

    Accessors

    • get router(): Router
    • Express router for health REST API component

      +

      Returns Router

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Service-1.html b/docs/classes/_mdf.js_service-registry._internal_.Service-1.html new file mode 100644 index 00000000..fdb24145 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Service-1.html @@ -0,0 +1,7 @@ +Service | @mdf.js

    Service class

    +

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Service-2.html b/docs/classes/_mdf.js_service-registry._internal_.Service-2.html new file mode 100644 index 00000000..3ae572b9 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Service-2.html @@ -0,0 +1,7 @@ +Service | @mdf.js

    Service class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return all the actual metrics of this artifact

      +

      Parameters

      • jsonFormat: boolean

      Returns Promise<Response>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Service-3.html b/docs/classes/_mdf.js_service-registry._internal_.Service-3.html new file mode 100644 index 00000000..c788337e --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Service-3.html @@ -0,0 +1,7 @@ +Service | @mdf.js

    Service class

    +

    Constructors

    Methods

    Constructors

    Methods

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Service.html b/docs/classes/_mdf.js_service-registry._internal_.Service.html new file mode 100644 index 00000000..c3c94451 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Service.html @@ -0,0 +1,11 @@ +Service | @mdf.js

    Service class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return the configuration object

      +

      Returns Promise<Record<string, any>>

    • Return the readme object

      +

      Returns Promise<undefined | string>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerAccessors.html b/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerAccessors.html new file mode 100644 index 00000000..ccbfd398 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerAccessors.html @@ -0,0 +1,70 @@ +SettingsManagerAccessors | @mdf.js

    Class SettingsManagerAccessors<CustomSettings>

    SettingsManager is responsible for managing the application's settings, including the +configuration for the service registry and custom settings specified by the user. It extends +EventEmitter to allow for emitting events related to settings management and implements the +Service interface from the Layer.App namespace, indicating its role in the application's service +architecture. It utilizes configuration managers for both service registry and custom settings, +supporting dynamic loading and management of these configurations.

    +

    Additionally, it can load application metadata from package.json and README.md content, providing +a centralized way to access application information and documentation.

    +

    Type Parameters

    Hierarchy (View Summary)

    Implements

    Constructors

    Properties

    instanceId: string = ...

    Instance identifier

    +
    package?: Package

    Package version info

    +
    readme?: string

    Readme file content

    +

    Accessors

    • get componentId(): string
    • Returns string

      Service instance identifier

      +
    • get error(): undefined | Multi
    • Returns undefined | Multi

      A validation error, if exist, in the configuration loaded

      +
    • get isPrimary(): boolean
    • Returns boolean

      If the application is the primary node in the cluster

      +
    • get isWorker(): boolean
    • Returns boolean

      If the application is a worker node in the cluster

      +
    • get name(): string
    • Returns string

      Service name

      +
    • get namespace(): undefined | string
    • Returns undefined | string

      Application namespace

      +
    • get release(): string
    • Returns string

      Application release

      +
    • get router(): Router
    • Returns Router

      Express router with access to config information

      +
    • get status(): "pass" | "fail" | "warn"
    • Returns "pass" | "fail" | "warn"

      Settings manager status

      +

    Methods

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerBase.html b/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerBase.html new file mode 100644 index 00000000..4e3ba368 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.SettingsManagerBase.html @@ -0,0 +1,30 @@ +SettingsManagerBase | @mdf.js

    Class SettingsManagerBase<CustomSettings>

    SettingsManager is responsible for managing the application's settings, including the +configuration for the service registry and custom settings specified by the user. It extends +EventEmitter to allow for emitting events related to settings management and implements the +Service interface from the Layer.App namespace, indicating its role in the application's service +architecture. It utilizes configuration managers for both service registry and custom settings, +supporting dynamic loading and management of these configurations.

    +

    Additionally, it can load application metadata from package.json and README.md content, providing +a centralized way to access application information and documentation.

    +

    Type Parameters

    Hierarchy (View Summary)

    Constructors

    Properties

    Methods

    Constructors

    Properties

    instanceId: string = ...

    Instance identifier

    +
    package?: Package

    Package version info

    +
    readme?: string

    Readme file content

    +

    Methods

    • Close the underlying configuration providers

      +

      Returns Promise<void>

    • Start the underlying configuration providers

      +

      Returns Promise<void>

    • Stop the underlying configuration providers

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_service-registry._internal_.Validator.html b/docs/classes/_mdf.js_service-registry._internal_.Validator.html new file mode 100644 index 00000000..79180b38 --- /dev/null +++ b/docs/classes/_mdf.js_service-registry._internal_.Validator.html @@ -0,0 +1,8 @@ +Validator | @mdf.js

    Validator class

    +

    Constructors

    Methods

    Constructors

    Methods

    • Return all the actual metrics of this artifact

      +

      Parameters

      • request: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        HTTP request express object

        +
      • response: Response<any, Record<string, any>>

        HTTP response express object

        +
      • next: NextFunction

        Next express middleware function

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_service-setup-provider.ConfigManager.html b/docs/classes/_mdf.js_service-setup-provider.ConfigManager.html new file mode 100644 index 00000000..55afe6c9 --- /dev/null +++ b/docs/classes/_mdf.js_service-setup-provider.ConfigManager.html @@ -0,0 +1,34 @@ +ConfigManager | @mdf.js

    Class responsible of file management, both configuration file as validator files

    +

    Type Parameters

    • SystemConfig extends Record<string, any> = Record<string, any>

    Constructors

    Properties

    config: SystemConfig

    Final configuration

    +
    defaultConfig: Partial<SystemConfig> = {}

    Default configuration

    +
    envConfig: Partial<SystemConfig> = {}

    Environment configuration

    +
    nonDisclosureConfig: Partial<SystemConfig>

    Final configuration without environment variables

    +
    presets: Record<string, Partial<SystemConfig>> = {}

    Presets configuration map

    +

    Accessors

    • get error(): undefined | Multi
    • Validation error, if exist

      +

      Returns undefined | Multi

    • get isErrored(): boolean
    • Flag to indicate that the final configuration has some errors

      +

      Returns boolean

    • get preset(): undefined | string
    • Return the preset used to create the final configuration

      +

      Returns undefined | string

    • get schema(): undefined | string
    • Return the schema used to validate the final configuration

      +

      Returns undefined | string

    Methods

    • Gets the value at path of object. If the resolved value is undefined, the defaultValue is +returned in its place.

      +

      Type Parameters

      • T

        Type of the property to return

        +

      Parameters

      • path: string | string[]

        path to the property to get

        +
      • OptionaldefaultValue: any

        default value to return if the property is not found

        +

      Returns undefined | T

    • Gets the value at path of object. If the resolved value is undefined, the defaultValue is +returned in its place.

      +

      Type Parameters

      • P extends string | number | symbol

      Parameters

      • key: P

        path to the property to get

        +
      • OptionaldefaultValue: any

        default value to return if the property is not found

        +

      Returns undefined | SystemConfig[P]

    diff --git a/docs/classes/_mdf.js_service-setup-provider.Setup.Port.html b/docs/classes/_mdf.js_service-setup-provider.Setup.Port.html new file mode 100644 index 00000000..8cadfc43 --- /dev/null +++ b/docs/classes/_mdf.js_service-setup-provider.Setup.Port.html @@ -0,0 +1,60 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Type Parameters

    • T extends Record<string, any> = Record<string, any>

      Underlying client type, this is, the real client of the wrapped provider

      +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    • Implementation of functionalities of an Elastic port instance.

      +

      Type Parameters

      • T extends Record<string, any> = Record<string, any>

      Parameters

      • config: Setup.Config<T>

        Port configuration options

        +
      • logger: LoggerInstance

        Port logger, to be used internally

        +
      • Optionalname: string

        Port name, to be used internally

        +

      Returns Setup.Port<T>

    Accessors

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_socket-client-provider.SocketIOClient.Port.html b/docs/classes/_mdf.js_socket-client-provider.SocketIOClient.Port.html new file mode 100644 index 00000000..f1f849a8 --- /dev/null +++ b/docs/classes/_mdf.js_socket-client-provider.SocketIOClient.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Socket<DefaultEventsMap, DefaultEventsMap>
    • Return the underlying port instance

      +

      Returns Socket<DefaultEventsMap, DefaultEventsMap>

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_socket-server-provider.SocketIOServer.Port.html b/docs/classes/_mdf.js_socket-server-provider.SocketIOServer.Port.html new file mode 100644 index 00000000..30d95cfe --- /dev/null +++ b/docs/classes/_mdf.js_socket-server-provider.SocketIOServer.Port.html @@ -0,0 +1,58 @@ +Port | @mdf.js

    This is the class that should be extended to implement a new specific Port.

    +

    This class implements some util logic to facilitate the creation of new Ports, for this reason is +exposed as abstract class, instead of an interface. The basic operations that already implemented +in the class are:

    +
      +
    • Health.Checks management: using the Port.addCheck method is +possible to include new observed values that will be used in the observability layers.
    • +
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error +traceability.
    • +
    • Establish the context for the logger to simplify the identification of the port in the logs, +this is, it's not necessary to indicate the uuid and context in each logging function call.
    • +
    • Store the configuration PortConfig previously validated by the Manager.
    • +
    +

    What the user of this class should develop in the specific port:

    +
      +
    • The Port.start method, which is responsible initialize or stablish the connection to +the resources.
    • +
    • The Port.stop method, which is responsible stop services or disconnect from the +resources.
    • +
    • The Port.close method, which is responsible to destroy the services, resources or +perform a simple disconnection.
    • +
    • The Port.state property, a boolean value that indicates if the port is connected +healthy (true) or not (false).
    • +
    • The Port.client property, that return the PortClient instance that is used to +interact with the resources.
    • +
    +

    class diagram

    +

    In the other hand, this class extends the EventEmitter class, so it's possible to emit +events to notify the status of the port:

    +
      +
    • error: should be emitted to notify errors in the resource management or access, this will +not change the provider state, but the error will be registered in the observability layers.
    • +
    • closed: should be emitted if the access to the resources is not longer possible. This +event should not be emitted if Port.stop or Port.close methods are used.
    • +
    • unhealthy: should be emitted when the port has limited access to the resources.
    • +
    • healthy: should be emitted when the port has recovered the access to the resources.
    • +
    +

    class diagram

    +

    Check some examples of implementation in:

    + +

    Hierarchy (View Summary)

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get client(): Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>
    • Return the underlying port instance

      +

      Returns Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>

    • get state(): boolean
    • Return the port state as a boolean value, true if the port is available, false in otherwise

      +

      Returns boolean

    Methods

    • Close the port instance

      +

      Returns Promise<void>

    • Initialize the port instance

      +

      Returns Promise<void>

    • Stop the port instance

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_tasks.Group.html b/docs/classes/_mdf.js_tasks.Group.html new file mode 100644 index 00000000..d63fb907 --- /dev/null +++ b/docs/classes/_mdf.js_tasks.Group.html @@ -0,0 +1,60 @@ +Group | @mdf.js

    Class Group<T, U>

    Represents the task handler

    +

    Type Parameters

    • T
    • U

    Hierarchy (View Summary)

    Constructors

    • Create a new task handler for a group of tasks

      +

      Type Parameters

      • T
      • U

      Parameters

      • tasks: TaskHandler<T, U>[]

        The tasks to execute

        +
      • Optionaloptions: TaskOptions<U>

        The options for the task

        +
      • OptionalatLeastOne: boolean

        If at least one task must succeed to consider the group as successful +execution, in other case, all the tasks must succeed

        +

      Returns Group<T, U>

    Properties

    createdAt: Date

    Date when the task was created

    +
    priority: number

    Task priority

    +
    taskId: string

    Task identifier, defined by the user

    +
    uuid: string

    Unique task identification, unique for each task

    +
    weight: number

    Task weight

    +

    Accessors

    • get metadata(): MetaData
    • Return the metadata of the task

      +

      Returns MetaData

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Execute the task

      +

      Returns Promise<(null | T)[]>

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<(null | T)[]>

        Done event handler

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_tasks.Limiter.html b/docs/classes/_mdf.js_tasks.Limiter.html new file mode 100644 index 00000000..be1e1756 --- /dev/null +++ b/docs/classes/_mdf.js_tasks.Limiter.html @@ -0,0 +1,131 @@ +Limiter | @mdf.js

    A limiter is a queue system that allows you to control the rate of job processing. It can be used +to limit the number of concurrent jobs, the delay between each job, the maximum number of jobs in +the queue, and the strategy to use when the queue length reaches highWater.

    +

    Hierarchy (View Summary)

    Constructors

    Accessors

    • get pending(): number
    • Returns the number of pending jobs

      +

      Returns number

    • get size(): number
    • Returns the number of jobs in the queue

      +

      Returns number

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Register an event listener over the refill event, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Register an event listener over the seed event, which is emitted when queue is empty and a +new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Executes a task and returns a promise that resolves when the task is done

      +

      Type Parameters

      • T
      • U

      Parameters

      Returns Promise<T>

      A promise that resolves when the task is done

      +
    • Executes a task and returns a promise that resolves when the task is done

      +

      Type Parameters

      • T
      • U

      Parameters

      Returns Promise<T>

      A promise that resolves when the task is done

      +
    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the refill event.

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the seed event.

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Register an event listener over the refill event, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Register an event listener over the seed event, which is emitted when queue is empty and a +new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a one-time event listener over the refill event, which is emitted when queue bucket +is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a one-time event listener over the seed event, which is emitted when queue is empty +and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Pipes the limiter to another limiter

      +

      Parameters

      Returns void

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a event listener over the refill event, at the beginning of the listeners array, +which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a event listener over the seed event, at the beginning of the listeners array, which +is emitted when queue is empty and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a one-time event listener over the refill event, at the beginning of the listeners +array, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a one-time event listener over the seed event, at the beginning of the listeners +array, which is emitted when queue is empty and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "refill"

        refill event

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "seed"

        seed event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the refill event.

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the seed event.

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Schedules a task to be executed by the limiter

      +

      Type Parameters

      • T
      • U

      Parameters

      Returns undefined | string

      The task handler

      +
    • Schedules a task to be executed by the limiter

      +

      Type Parameters

      • T
      • U

      Parameters

      Returns undefined | string

      The task handler

      +
    • Starts the limiter

      +

      Returns void

    • Stops the limiter

      +

      Returns void

    • Waits until the queue is empty

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_tasks.PollingExecutor.html b/docs/classes/_mdf.js_tasks.PollingExecutor.html new file mode 100644 index 00000000..5246861e --- /dev/null +++ b/docs/classes/_mdf.js_tasks.PollingExecutor.html @@ -0,0 +1,17 @@ +PollingExecutor | @mdf.js

    Polling manager

    +

    Hierarchy

    • EventEmitter
      • PollingExecutor

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get check(): Check<any>
    • Return the stats of the polling manager

      +

      Returns Check<any>

    Methods

    • Emitted on every error

      +

      Parameters

      • event: "error"
      • listener: (error: Crash | Multi) => void

      Returns this

    • Emitted on every state change

      +

      Parameters

      • event: "status"
      • listener: (status: "pass" | "fail" | "warn") => void

      Returns this

    • Emitted when a task has ended

      +

      Parameters

      • event: "done"
      • listener: (uuid: string, result: any, meta: MetaData, error?: Crash | Multi) => void

      Returns this

    • Start the polling manager

      +

      Returns void

    • Stop the polling manager

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_tasks.Scheduler.html b/docs/classes/_mdf.js_tasks.Scheduler.html new file mode 100644 index 00000000..a10bf5ee --- /dev/null +++ b/docs/classes/_mdf.js_tasks.Scheduler.html @@ -0,0 +1,71 @@ +Scheduler | @mdf.js

    Class Scheduler<Result, Binding, PollingGroups>

    A scheduler is a service that manages the execution of tasks in a controlled and efficient way. +It is responsible for managing the resources and the rate limits of the tasks, and for emitting +events when the tasks are done or when an error occurs.

    +

    Type Parameters

    Hierarchy

    • EventEmitter
      • Scheduler

    Implements

    Constructors

    Properties

    componentId: string = ...

    Provider unique identifier for trace purposes

    +
    metrics: Registry<"text/plain; version=0.0.4; charset=utf-8"> = ...

    Metrics registry

    +
    metricsDefinitions: MetricsDefinitions = ...

    Metrics definitions

    +
    name: string

    The name of the scheduler

    +

    Accessors

    • get status(): "pass" | "fail" | "warn"
    • Get the health status for the scheduler

      +

      Returns "pass" | "fail" | "warn"

    Methods

    • Add a listener for the done event, emitted when a task is done, with the result or the error.

      +

      Parameters

      Returns this

    • Cleanup the scheduler

      +

      Returns void

    • Close the scheduler

      +

      Returns Promise<void>

    • Drop a resource from the scheduler

      +

      Parameters

      • resource: string

        The resource to drop

        +

      Returns void

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      +

      Parameters

      • event: "error"

        error event

        +
      • listener: (error: Crash | Error | Multi) => void

        Error event listener

        +

      Returns this

    • Add a listener for the status event, emitted when the component status changes.

      +

      Parameters

      • event: "status"

        status event

        +
      • listener: (status: "pass" | "fail" | "warn") => void

        Status event listener

        +

      Returns this

    • Add a listener for the done event, emitted when a task is done, with the result or the error.

      +

      Parameters

      Returns this

    • Add a listener for the done event, emitted when a task is done, with the result or the error. +This is a one-time event, the listener will be removed after the first emission.

      +

      Parameters

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    • Start the scheduler

      +

      Returns Promise<void>

    • Stop the scheduler

      +

      Returns Promise<void>

    diff --git a/docs/classes/_mdf.js_tasks.Sequence.html b/docs/classes/_mdf.js_tasks.Sequence.html new file mode 100644 index 00000000..7d36b390 --- /dev/null +++ b/docs/classes/_mdf.js_tasks.Sequence.html @@ -0,0 +1,58 @@ +Sequence | @mdf.js

    Class Sequence<T, U>

    Represents the task handler

    +

    Type Parameters

    • T
    • U

    Hierarchy (View Summary)

    Constructors

    Properties

    createdAt: Date

    Date when the task was created

    +
    priority: number

    Task priority

    +
    taskId: string

    Task identifier, defined by the user

    +
    uuid: string

    Unique task identification, unique for each task

    +
    weight: number

    Task weight

    +

    Accessors

    • get metadata(): MetaData
    • Return the metadata of the task

      +

      Returns MetaData

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_tasks.Single.html b/docs/classes/_mdf.js_tasks.Single.html new file mode 100644 index 00000000..67200463 --- /dev/null +++ b/docs/classes/_mdf.js_tasks.Single.html @@ -0,0 +1,62 @@ +Single | @mdf.js

    Class Single<T, U>

    Represents the task handler

    +

    Type Parameters

    • T
    • U

    Hierarchy (View Summary)

    Constructors

    Properties

    createdAt: Date

    Date when the task was created

    +
    priority: number

    Task priority

    +
    taskId: string

    Task identifier, defined by the user

    +
    uuid: string

    Unique task identification, unique for each task

    +
    weight: number

    Task weight

    +

    Accessors

    • get metadata(): MetaData
    • Return the metadata of the task

      +

      Returns MetaData

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: "done"

        done event

        +
      • listener: DoneListener<T>

        Done event handler

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_tasks.TaskHandler.html b/docs/classes/_mdf.js_tasks.TaskHandler.html new file mode 100644 index 00000000..49317904 --- /dev/null +++ b/docs/classes/_mdf.js_tasks.TaskHandler.html @@ -0,0 +1,57 @@ +TaskHandler | @mdf.js

    Class TaskHandler<Result, Binded>Abstract

    Represents the task handler

    +

    Type Parameters

    • Result
    • Binded

    Hierarchy (View Summary)

    Constructors

    Properties

    createdAt: Date

    Date when the task was created

    +
    priority: number

    Task priority

    +
    taskId: string

    Task identifier, defined by the user

    +
    uuid: string

    Unique task identification, unique for each task

    +
    weight: number

    Task weight

    +

    Accessors

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      Returns this

    • Cancel the task

      +

      Parameters

      Returns void

    • Execute the task

      +

      Returns Promise<Result>

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      Returns this

    diff --git a/docs/classes/_mdf.js_tasks._internal_.LimiterStateHandler.html b/docs/classes/_mdf.js_tasks._internal_.LimiterStateHandler.html new file mode 100644 index 00000000..f0a37983 --- /dev/null +++ b/docs/classes/_mdf.js_tasks._internal_.LimiterStateHandler.html @@ -0,0 +1,104 @@ +LimiterStateHandler | @mdf.js

    Class LimiterStateHandlerAbstract

    A limiter state handler is a class that manages the state of a limiter, including the queue of tasks, +the concurrency, and the limiter options.

    +

    Hierarchy (View Summary)

    • EventEmitter

    Constructors

    Accessors

    • get pending(): number
    • Returns the number of pending jobs

      +

      Returns number

    • get size(): number
    • Returns the number of jobs in the queue

      +

      Returns number

    Methods

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Register an event listener over the refill event, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Register an event listener over the seed event, which is emitted when queue is empty and a +new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Clears the queue

      +

      Returns void

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the refill event.

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the seed event.

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Register an event listener over the done event, which is emitted when a task has ended, either +due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Register an event listener over the refill event, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Register an event listener over the seed event, which is emitted when queue is empty and a +new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a task has +ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a one-time event listener over the refill event, which is emitted when queue bucket +is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a one-time event listener over the seed event, which is emitted when queue is empty +and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, +which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a event listener over the refill event, at the beginning of the listeners array, +which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a event listener over the seed event, at the beginning of the listeners array, which +is emitted when queue is empty and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners +array, which is emitted when a task has ended, either due to completion or failure.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        Done event listener

        +

      Returns this

    • Registers a one-time event listener over the refill event, at the beginning of the listeners +array, which is emitted when queue bucket is refilled

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        Refill event listener

        +

      Returns this

    • Registers a one-time event listener over the seed event, at the beginning of the listeners +array, which is emitted when queue is empty and a new task is added

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to add

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "done"

        done event

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "refill"

        refill event

        +

      Returns this

    • Removes all listeners, or those of the specified event.

      +

      Parameters

      • Optionalevent: "seed"

        seed event

        +

      Returns this

    • Removes the specified listener from the listener array for the done event.

      +

      Parameters

      • event: string

        done event

        +
      • listener: DoneListener

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the refill event.

      +

      Parameters

      • event: "refill"

        refill event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    • Removes the specified listener from the listener array for the seed event.

      +

      Parameters

      • event: "seed"

        seed event

        +
      • listener: () => void

        The listener function to remove

        +

      Returns this

    diff --git a/docs/classes/_mdf.js_tasks._internal_.PollingManager.html b/docs/classes/_mdf.js_tasks._internal_.PollingManager.html new file mode 100644 index 00000000..33a342c4 --- /dev/null +++ b/docs/classes/_mdf.js_tasks._internal_.PollingManager.html @@ -0,0 +1,19 @@ +PollingManager | @mdf.js

    Hierarchy

    • EventEmitter
      • PollingManager

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get check(): Check<any>
    • Return the stats of the polling manager

      +

      Returns Check<any>

    Methods

    • Emitted on every error

      +

      Parameters

      • event: "error"
      • listener: (error: Crash | Multi) => void

      Returns this

    • Emitted when a task is passed to off, this means that the task has been disabled

      +

      Parameters

      Returns this

    • Emitted when a task is passed to slow cycle

      +

      Parameters

      Returns this

    • Emitted when a task is passed to fast cycle

      +

      Parameters

      Returns this

    • Emitted when a task has ended

      +

      Parameters

      • event: "done"
      • listener: (uuid: string, result: any, meta: MetaData, error?: Crash | Multi) => void

      Returns this

    • Emitted when a cycle has ended

      +

      Parameters

      Returns this

    • Emitted when a cycle has started

      +

      Parameters

      • event: "startCycle"
      • listener: () => void

      Returns this

    • Schedule the tasks to be executed

      +

      Returns void

    diff --git a/docs/classes/_mdf.js_tasks._internal_.PollingMetricsHandler.html b/docs/classes/_mdf.js_tasks._internal_.PollingMetricsHandler.html new file mode 100644 index 00000000..7f9788b9 --- /dev/null +++ b/docs/classes/_mdf.js_tasks._internal_.PollingMetricsHandler.html @@ -0,0 +1,22 @@ +PollingMetricsHandler | @mdf.js

    Constructors

    • Create a polling stats manager

      +

      Parameters

      • componentId: string

        Component identifier

        +
      • resource: string

        Resource identifier

        +
      • pollingGroup: PollingGroup

        Polling group assigned to this manager

        +
      • cyclesOnStats: number = DEFAULT_SCAN_CYCLES_ON_STATS

        Number of cycles on stats

        +
      • metrics: MetricsDefinitions

        Metrics instances

        +

      Returns PollingMetricsHandler

    Accessors

    • get check(): Check<any>
    • Get health check of the component

      +

      Returns Check<any>

    Methods

    • Add a task to the in progress stats

      +

      Parameters

      • taskId: string

        Task identifier

        +

      Returns void

    • Set the final point of a cycle

      +

      Returns void

    • Set the initial point of a cycle

      +

      Returns void

    • Remove a task from the in progress stats

      +

      Parameters

      • taskId: string

        Task identifier

        +
      • duration: number = 0

        Task duration

        +
      • Optionalerror: Crash | Multi

        Task error

        +

      Returns void

    diff --git a/docs/classes/_mdf.js_tasks._internal_.Queue.html b/docs/classes/_mdf.js_tasks._internal_.Queue.html new file mode 100644 index 00000000..8f0823d7 --- /dev/null +++ b/docs/classes/_mdf.js_tasks._internal_.Queue.html @@ -0,0 +1,38 @@ +Queue | @mdf.js

    Represents a queue

    +

    Hierarchy

    • EventEmitter
      • Queue

    Constructors

    Accessors

    Methods

    Constructors

    Accessors

    • get size(): number
    • Gets the size of the queue

      +

      Returns number

    Methods

    • Clears the queue

      +

      Returns void

    • Dequeues and returns the next job in the queue

      +

      Returns undefined | TaskHandler<any, any>

      The next job, or undefined if the queue is empty

      +
    • Drops the oldest job in the queue with the lowest priority if not priority is provided, and +drops the oldest job with a lower priority than the provided priority otherwise

      +

      Parameters

      • Optionalpriority: number

        The priority of the job to drop

        +

      Returns undefined | TaskHandler<any, any>

      The dropped job, or undefined if the queue is empty

      +

      The queue is ordered by priority and age, this means that first jobs have a higher priority, +and if several jobs have the same priority, the oldest one its earlier in the queue

      +
    • Enqueues a job with optional priority

      +

      Parameters

      Returns boolean

    • Peeks the next job in the queue

      +

      Returns undefined | TaskHandler<any, any>

      The next job, or undefined if the queue is empty

      +
    • Emitted when a job is enqueued

      +

      Parameters

      • event: "enqueue"
      • listener: () => void

      Returns this

    • Emitted when a job is dequeued

      +

      Parameters

      • event: "dequeue"
      • listener: () => void

      Returns this

    • Emitted when a job is removed

      +

      Parameters

      • event: "removed"
      • listener: () => void

      Returns this

    • Emitted when the queue is cleared

      +

      Parameters

      • event: "cleared"
      • listener: () => void

      Returns this

    • Emitted when the queue is blocked

      +

      Parameters

      • event: "blocked"
      • listener: () => void

      Returns this

    • Emitted when the queue is unblocked

      +

      Parameters

      • event: "unblocked"
      • listener: () => void

      Returns this

    • Emitted when the queue is empty

      +

      Parameters

      • event: "empty"
      • listener: () => void

      Returns this

    • Emitted when the bucket is refilled

      +

      Parameters

      • event: "refill"
      • listener: () => void

      Returns this

    • Emitted when the queue is empty and a job is enqueued

      +

      Parameters

      • event: "seed"
      • listener: () => void

      Returns this

    • Removes a job from the queue

      +

      Parameters

      • uuid: string

        The uuid of the job to remove

        +

      Returns undefined | TaskHandler<any, any>

    diff --git a/docs/classes/_mdf.js_tasks._internal_.RetryManager.html b/docs/classes/_mdf.js_tasks._internal_.RetryManager.html new file mode 100644 index 00000000..39205089 --- /dev/null +++ b/docs/classes/_mdf.js_tasks._internal_.RetryManager.html @@ -0,0 +1,14 @@ +RetryManager | @mdf.js

    Constructors

    • Create a retry manager

      +

      Parameters

      • limiterDelay: number

        Limiter delay

        +
      • pollingGroup: string

        Polling group

        +
      • logger: LoggerInstance

        Logger

        +

      Returns RetryManager

    Methods

    diff --git a/docs/classes/_mdf_js_core.Jobs.JobHandler.html b/docs/classes/_mdf_js_core.Jobs.JobHandler.html deleted file mode 100644 index dad83427..00000000 --- a/docs/classes/_mdf_js_core.Jobs.JobHandler.html +++ /dev/null @@ -1,83 +0,0 @@ -JobHandler | @mdf.js

    Class JobHandler<Type, Data, CustomHeaders, CustomOptions>

    JobHandler class

    -

    Type Parameters

    • Type extends string

      Job type, used as selector for strategies in job processors

      -
    • Data = unknown

      Job payload

      -
    • CustomHeaders extends Record<string, any> = AnyHeaders

      Custom headers, used to pass specific information for job processors

      -
    • CustomOptions extends Record<string, any> = AnyOptions

      Custom options, used to pass specific information for job processors

      -

    Hierarchy

    • EventEmitter
      • JobHandler

    Implements

    Constructors

    Properties

    createdAt: Date

    Date object with the timestamp when the job was created

    -
    jobUserId: string

    User job request identifier, defined by the user

    -
    jobUserUUID: string

    Unique user job request identification, based on jobUserId

    -

    Job meta information, used to pass specific information for job processors

    -
    type: Type

    Job type, used as selector for strategies in job processors

    -
    uuid: string

    Unique job processing identification

    -

    Accessors

    • get errors(): undefined | Multi
    • Errors raised during the job

      -

      Returns undefined | Multi

    • get hasErrors(): boolean
    • True if the job task raised any error

      -

      Returns boolean

    • get processTime(): number
    • Return the process time in msec

      -

      Returns number

    Methods

    • Add a new error in the job

      -

      Parameters

      • error: Multi | Crash

        error to be added to the job

        -

      Returns void

    • Register an event listener over the done event, which is emitted when a job has ended, either -due to completion or failure.

      -

      Parameters

      Returns this

    • Notify the results of the process

      -

      Parameters

      • Optionalerror: Crash

        conditional parameter for error notification

        -

      Returns void

    • Removes the specified listener from the listener array for the done event.

      -

      Parameters

      Returns this

    • Register an event listener over the done event, which is emitted when a job has ended, either -due to completion or failure.

      -

      Parameters

      Returns this

    • Registers a one-time event listener over the done event, which is emitted when a job has -ended, either due to completion or failure.

      -

      Parameters

      Returns this

    • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a job has ended, either due to completion or failure.

      -

      Parameters

      Returns this

    • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a job has ended, either due to completion or failure.

      -

      Parameters

      Returns this

    • Removes all listeners, or those of the specified event.

      -

      Parameters

      • Optionalevent: "done"

        done event

        -

      Returns this

    • Removes the specified listener from the listener array for the done event.

      -

      Parameters

      Returns this

    diff --git a/docs/classes/_mdf_js_core.Layer.Provider.Manager.html b/docs/classes/_mdf_js_core.Layer.Provider.Manager.html deleted file mode 100644 index 980823bb..00000000 --- a/docs/classes/_mdf_js_core.Layer.Provider.Manager.html +++ /dev/null @@ -1,96 +0,0 @@ -Manager | @mdf.js

    Class Manager<PortClient, PortConfig, PortInstance>

    Provider Manager wraps a specific port created by the extension of the Port abstract -class, instrumenting it with the necessary logic to manage:

    - -

    class diagram

    -
      -
    • Merge and validate the configuration of the provider represented by the generic type -PortConfig. The manager configuration object ProviderOptions has a validation -property that represent a structure of type PortConfigValidationStruct where default -values, environment based and a Joi validation object are -defined. During the initialization process, the manager will merge all the sources of -configuration (default, environment and specific) and validate the result against the Joi schema. -So, the order of priority of the configuration sources is: specific, environment and default. -If the validation fails, the manager will use the default values and emit an error that will be -managed by the observability layer.
    • -
    -

    Port class, this is, the class that extends the Port abstract class

    -

    Type Parameters

    • PortClient

      Underlying client type, this is, the real client of the wrapped provider

      -
    • PortConfig

      Port configuration object, could be an extended version of the client config

      -
    • PortInstance extends Layer.Provider.Port<PortClient, PortConfig>

    Hierarchy

    • EventEmitter
      • Manager

    Implements

    Constructors

    Properties

    componentId: string

    Provider unique identifier for trace purposes

    -
    config: PortConfig

    Port configuration

    -

    Accessors

    • get date(): string
    • Timestamp of actual state in ISO format, when the current state was reached

      -

      Returns string

    • get error(): undefined | Multi | Crash
    • Return the errors in the provider

      -

      Returns undefined | Multi | Crash

    • get name(): string
    • Provider name

      -

      Returns string

    • get state(): "error" | "running" | "stopped"
    • Provider state

      -

      Returns "error" | "running" | "stopped"

    • get status(): "pass" | "fail" | "warn"
    • Provider status

      -

      Returns "pass" | "fail" | "warn"

    Methods

    • Add a listener for the error event, emitted when the component detects an error.

      -

      Parameters

      • event: "error"

        error event

        -
      • listener: ((error: Multi | Crash) => void)

        Error event listener

        -
          • (error): void
          • Parameters

            Returns void

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      -

      Parameters

      • event: "status"

        status event

        -
      • listener: ((status: ProviderStatus) => void)

        Status event listener

        -

      Returns this

    • Close the provider: release resources, connections ...

      -

      Returns Promise<void>

    • Error state: wait for new state of to fix the actual degraded stated

      -

      Parameters

      • error: Error | Crash

        Cause ot this fail transition

        -

      Returns Promise<void>

    • Removes the specified listener from the listener array for the error event.

      -

      Parameters

      • event: "error"

        error event

        -
      • listener: ((error: Multi | Crash) => void)

        Error event listener

        -
          • (error): void
          • Parameters

            Returns void

      Returns this

    • Removes the specified listener from the listener array for the status event.

      -

      Parameters

      • event: "status"

        status event

        -
      • listener: ((status: ProviderStatus) => void)

        Status event listener

        -

      Returns this

    • Add a listener for the error event, emitted when the component detects an error.

      -

      Parameters

      • event: "error"

        error event

        -
      • listener: ((error: Multi | Crash) => void)

        Error event listener

        -
          • (error): void
          • Parameters

            Returns void

      Returns this

    • Add a listener for the status event, emitted when the component changes its status.

      -

      Parameters

      • event: "status"

        status event

        -
      • listener: ((status: ProviderStatus) => void)

        Status event listener

        -

      Returns this

    • Add a listener for the error event, emitted when the component detects an error. This is a -one-time event, the listener will be removed after the first emission.

      -

      Parameters

      • event: "error"

        error event

        -
      • listener: ((error: Multi | Crash) => void)

        Error event listener

        -
          • (error): void
          • Parameters

            Returns void

      Returns this

    • Add a listener for the status event, emitted when the component changes its status. This is a -one-time event, the listener will be removed after the first emission.

      -

      Parameters

      • event: "status"

        status event

        -
      • listener: ((status: ProviderStatus) => void)

        Status event listener

        -

      Returns this

    • Removes all listeners, or those of the specified event.

      -

      Parameters

      • Optionalevent: "error"

        error event

        -

      Returns this

    • Removes the specified listener from the listener array for the error event.

      -

      Parameters

      • event: "error"

        error event

        -
      • listener: ((error: Multi | Crash) => void)

        Error event listener

        -
          • (error): void
          • Parameters

            Returns void

      Returns this

    • Removes the specified listener from the listener array for the status event.

      -

      Parameters

      • event: "status"

        status event

        -
      • listener: ((status: ProviderStatus) => void)

        Status event listener

        -

      Returns this

    • Initialize the process: internal jobs, external dependencies connections ...

      -

      Returns Promise<void>

    • Stop the process: internal jobs, external dependencies connections ...

      -

      Returns Promise<void>

    diff --git a/docs/classes/_mdf_js_core.Layer.Provider.Port.html b/docs/classes/_mdf_js_core.Layer.Provider.Port.html deleted file mode 100644 index 0392432d..00000000 --- a/docs/classes/_mdf_js_core.Layer.Provider.Port.html +++ /dev/null @@ -1,131 +0,0 @@ -Port | @mdf.js

    Class Port<PortClient, PortConfig>Abstract

    This is the class that should be extended to implement a new specific Port.

    -

    This class implements some util logic to facilitate the creation of new Ports, for this reason is -exposed as abstract class, instead of an interface. The basic operations that already implemented -in the class are:

    -
      -
    • Health.Checks management: using the Port.addCheck method is -possible to include new observed values that will be used in the observability layers.
    • -
    • Create a Port.uuid unique identifier for the port instance, this uuid is used in error -traceability.
    • -
    • Establish the context for the logger to simplify the identification of the port in the logs, -this is, it's not necessary to indicate the uuid and context in each logging function call.
    • -
    • Store the configuration PortConfig previously validated by the Manager.
    • -
    -

    What the user of this class should develop in the specific port:

    -
      -
    • The Port.start method, which is responsible initialize or stablish the connection to -the resources.
    • -
    • The Port.stop method, which is responsible stop services or disconnect from the -resources.
    • -
    • The Port.close method, which is responsible to destroy the services, resources or -perform a simple disconnection.
    • -
    • The Port.state property, a boolean value that indicates if the port is connected -healthy (true) or not (false).
    • -
    • The Port.client property, that return the PortClient instance that is used to -interact with the resources.
    • -
    -

    class diagram

    -

    In the other hand, this class extends the EventEmitter class, so it's possible to emit -events to notify the status of the port:

    -
      -
    • error: should be emitted to notify errors in the resource management or access, this will -not change the provider state, but the error will be registered in the observability layers.
    • -
    • closed: should be emitted if the access to the resources is not longer possible. This -event should not be emitted if Port.stop or Port.close methods are used.
    • -
    • unhealthy: should be emitted when the port has limited access to the resources.
    • -
    • healthy: should be emitted when the port has recovered the access to the resources.
    • -
    -

    class diagram

    -

    Check some examples of implementation in:

    - -

    Type Parameters

    • PortClient

      Underlying client type, this is, the real client of the wrapped provider

      -
    • PortConfig

      Port configuration object, could be an extended version of the client config

      -

    Hierarchy

    • EventEmitter
      • Port

      Constructors

      Properties

      Accessors

      Methods

      Constructors

      Properties

      config: PortConfig

      Port configuration options

      -
      name: string

      Port name, to be used as identifier

      -
      uuid: string = ...

      Port unique identifier for trace purposes

      -

      Accessors

      • get checks(): Record<string, Check<any>[]>
      • Return the actual checks

        -

        Returns Record<string, Check<any>[]>

      • get state(): boolean
      • Return the port state as a boolean value, true if the port is available, false in otherwise

        -

        Returns boolean

      Methods

      • Update or add a check measure. -This should be used to inform about the state of resources behind the Port, for example memory -usage, CPU usage, etc.

        -

        The new check will be taking into account in the overall health status. -The new check will be included in the checks object with the key indicated in the param -measure.* If this key already exists, the componentId of the check parameter will be -checked, if there is a check with the same componentId in the array, the check will be -updated, in other case the new check will be added to the existing array.

        -

        The maximum number external checks is 100

        -

        Parameters

        • measure: string

          measure identification

          -
        • check: Check<any>

          check to be updated or included

          -

        Returns boolean

        true, if the check has been updated

        -
      • Close the port, making it no longer available

        -

        Returns Promise<void>

      • Emit an error event, to notify errors in the resource management or access, this will change -the provider state by the upper manager.

        -

        Parameters

        • event: "error"

          error event

          -
        • error: Multi | Crash

          Error to be notified to the upper manager

          -

        Returns boolean

      • Emit a closed event, to notify that the access to the resources is not longer possible. This -event should not be emitted if Port.stop or Port.close methods are used. This -event will change the provider state by the upper manager.

        -

        Parameters

        • event: "closed"

          closed event

          -
        • Optionalerror: Multi | Crash

          Error to be notified to the upper manager, if any

          -

        Returns boolean

      • Emit an unhealthy event, to notify that the port has limited access to the resources. This -event will change the provider state by the upper manager.

        -

        Parameters

        • event: "unhealthy"

          unhealthy event

          -
        • error: Multi | Crash

          Error to be notified to the upper manager

          -

        Returns boolean

      • Emit a healthy event, to notify that the port has recovered the access to the resources. This -event will change the provider state by the upper manager.

        -

        Parameters

        • event: "healthy"

          healthy event

          -

        Returns boolean

      • Add a listener for the error event, emitted when the component detects an error.

        -

        Parameters

        • event: "error"

          error event

          -
        • listener: ((error: Multi | Crash) => void)

          Error event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the closed event, emitted when the port resources are no longer available

        -

        Parameters

        • event: "closed"

          closed event

          -
        • listener: ((error?: Multi | Crash) => void)

          Closed event listener

          -
            • (error?): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the unhealthy event, emitted when the port has limited access to the -resources

        -

        Parameters

        • event: "unhealthy"

          unhealthy event

          -
        • listener: ((error: Multi | Crash) => void)

          Unhealthy event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the healthy event, emitted when the port has recovered the access to the -resources

        -

        Parameters

        • event: "healthy"

          healthy event

          -
        • listener: (() => void)

          Healthy event listener

          -
            • (): void
            • Returns void

        Returns this

      • Add a listener for the error event, emitted when the component detects an error. This is a -one-time event, the listener will be removed after the first emission.

        -

        Parameters

        • event: "error"

          error event

          -
        • listener: ((error: Multi | Crash) => void)

          Error event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the closed event, emitted when the port resources are no longer available. -This is a one-time event, the listener will be removed after the first emission.

        -

        Parameters

        • event: "closed"

          closed event

          -
        • listener: ((error?: Multi | Crash) => void)

          Closed event listener

          -
            • (error?): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the unhealthy event, emitted when the port has limited access to the -resources. This is a one-time event, the listener will be removed after the first emission.

        -

        Parameters

        • event: "unhealthy"

          unhealthy event

          -
        • listener: ((error: Multi | Crash) => void)

          Unhealthy event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the healthy event, emitted when the port has recovered the access to the -resources. This is a one-time event, the listener will be removed after the first emission.

        -

        Parameters

        • event: "healthy"

          healthy event

          -
        • listener: (() => void)

          Healthy event listener

          -
            • (): void
            • Returns void

        Returns this

      • Start the port, making it available

        -

        Returns Promise<void>

      • Stop the port, making it unavailable

        -

        Returns Promise<void>

      diff --git a/docs/classes/_mdf_js_crash.Boom.html b/docs/classes/_mdf_js_crash.Boom.html deleted file mode 100644 index 0f2af003..00000000 --- a/docs/classes/_mdf_js_crash.Boom.html +++ /dev/null @@ -1,62 +0,0 @@ -Boom | @mdf.js

      Improved error handling in REST-API interfaces

      -

      Boom helps us with error responses (HTTP Codes 3XX-5XX) within our REST-API interface by -providing us with some tools:

      -
        -
      • Helpers for the rapid generation of standard responses.
      • -
      • Association of errors and their causes in a hierarchical way.
      • -
      • Adaptation of validation errors of the Joi library.
      • -
      -

      In addition, in combination with the Multi error types, errors in validation processes, and -Crash, standard application errors, it allows a complete management of the different types of -errors in our backend.

      -

      Hierarchy

      • Base
        • Boom

      Constructors

      Properties

      Accessors

      Methods

      Constructors

      • Create a new Boom error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • uuid: string

          unique identifier for this particular occurrence of the problem

          -
        • code: number = 500

          HTTP Standard error code

          -
        • Optionaloptions: BoomOptions

          enhanced error options

          -

        Returns Boom

      Properties

      date: Date

      Error date

      -
      name: string = 'BaseError'

      Error name (type)

      -
      subject: string

      Error subject, 'common' as default

      -

      Accessors

      • get cause(): undefined | Cause
      • Cause source of error

        -

        Returns undefined | Cause

      • get info(): undefined | Record<string, unknown>
      • Return the info object for this error

        -

        Returns undefined | Record<string, unknown>

      • get isBoom(): boolean
      • Boom error

        -

        Returns boolean

      • Links that leads to further details about this particular occurrence of the problem. -A link MUST be represented as either:

        -
          -
        • self: a string containing the link’s URL
        • -
        • related: an object (“link object”) which can contain the following members: -
            -
          • href: a string containing the link’s URL.
          • -
          • meta: a meta object containing non-standard meta-information about the link.
          • -
          -
        • -
        -

        Returns undefined | Links

      • get resource(): undefined | APISource
      • Object with the key information of the requested resource in the REST API context

        -

        Returns undefined | APISource

      • get source(): undefined | APISource
      • Object with the key information of the requested resource in the REST API context

        -

        Returns undefined | APISource

          -
        • source has been deprecated, use resource instead
        • -
        -
      • get status(): number
      • Boom error code

        -

        Returns number

      • get uuid(): string
      • Return the unique identifier associated to this instance

        -

        Returns string

      Methods

      • Transform joi Validation error in a Boom error

        -

        Parameters

        Returns void

      • Return a string formatted as name:message

        -

        Returns string

      • Get the trace of this hierarchy of errors

        -

        Returns string[]

      diff --git a/docs/classes/_mdf_js_crash.Crash.html b/docs/classes/_mdf_js_crash.Crash.html deleted file mode 100644 index 0e954efa..00000000 --- a/docs/classes/_mdf_js_crash.Crash.html +++ /dev/null @@ -1,61 +0,0 @@ -Crash | @mdf.js

      Improved handling of standard errors.

      -

      Crash helps us manage standard errors within our application by providing us with some tools:

      -
        -
      • Association of errors and their causes in a hierarchical way.
      • -
      • Simple search for root causes within the hierarchy of errors.
      • -
      • Stack management, both of the current instance of the error, and of the causes.
      • -
      • Facilitate error logging.
      • -
      -

      In addition, in combination with the Multi error types, errors in validation processes, and Boom, -errors for the REST-API interfaces, it allows a complete management of the different types of -errors in our backend.

      -

      Hierarchy

      • Base
        • Crash

      Constructors

      • Create a new Crash error instance

        -

        Parameters

        • message: string

          human friendly error message

          -

        Returns Crash

      • Create a new Crash error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • options: CrashOptions

          enhanced error options

          -

        Returns Crash

      • Create a new Crash error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • uuid: string

          unique identifier for this particular occurrence of the problem

          -

        Returns Crash

      • Create a new Crash error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • uuid: string

          unique identifier for this particular occurrence of the problem

          -
        • options: CrashOptions

          enhanced error options

          -

        Returns Crash

      Properties

      date: Date

      Error date

      -
      name: string = 'BaseError'

      Error name (type)

      -
      subject: string

      Error subject, 'common' as default

      -

      Accessors

      • get cause(): undefined | Cause
      • Cause source of error

        -

        Returns undefined | Cause

      • get info(): undefined | Record<string, unknown>
      • Return the info object for this error

        -

        Returns undefined | Record<string, unknown>

      • get isCrash(): boolean
      • Determine if this instance is a Crash error

        -

        Returns boolean

      • get uuid(): string
      • Return the unique identifier associated to this instance

        -

        Returns string

      Methods

      • Look in the nested causes of the error and return the first occurrence of a cause with the -indicated name

        -

        Parameters

        • name: string

          name of the error to search for

          -

        Returns undefined | Cause

        the cause, if there is any present with that name

        -
      • Returns a full stack of the error and causes hierarchically. The string contains the -description of the point in the code at which the Error/Crash was instantiated

        -

        Returns undefined | string

      • Check if there is any cause in the stack with the indicated name

        -

        Parameters

        • name: string

          name of the error to search for

          -

        Returns boolean

        Boolean value as the result of the search

        -
      • Return a string formatted as name:message

        -

        Returns string

      • Get the trace of this hierarchy of errors

        -

        Returns string[]

      • Check if an object is a valid Crash or Multi error

        -

        Parameters

        • error: unknown

          error to be checked

          -
        • Optionaluuid: string

          Optional uuid to be used instead of a random one.

          -

        Returns Crash | Multi

      diff --git a/docs/classes/_mdf_js_crash.Multi.html b/docs/classes/_mdf_js_crash.Multi.html deleted file mode 100644 index 6a32efa4..00000000 --- a/docs/classes/_mdf_js_crash.Multi.html +++ /dev/null @@ -1,71 +0,0 @@ -Multi | @mdf.js

      Improved handling of validation errors.

      -

      Multi helps us to manage validation or information transformation errors, in other words, it -helps us manage any process that may generate multiple non-hierarchical errors (an error is not a -direct consequence of the previous one) by providing us with some tools:

      -
        -
      • Management of the error stack.
      • -
      • Simple search for root causes within the error stack.
      • -
      • Stack management, both of the current instance of the error, and of the causes.
      • -
      • Facilitate error logging.
      • -
      -

      Furthermore, in combination with the types of error Boom, errors for the REST-API interfaces, and -Crash, standard application errors, it allows a complete management of the different types of -errors in our backend.

      -

      Hierarchy

      • Base
        • Multi

      Constructors

      • Create a new Multi error

        -

        Parameters

        • message: string

          human friendly error message

          -

        Returns Multi

      • Create a new Multi error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • options: MultiOptions

          enhanced error options

          -

        Returns Multi

      • Create a new Multi error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • uuid: string

          unique identifier for this particular occurrence of the problem

          -

        Returns Multi

      • Create a new Multi error

        -

        Parameters

        • message: string

          human friendly error message

          -
        • uuid: string

          unique identifier for this particular occurrence of the problem

          -
        • options: MultiOptions

          enhanced error options

          -

        Returns Multi

      Properties

      date: Date

      Error date

      -
      name: string = 'BaseError'

      Error name (type)

      -
      subject: string

      Error subject, 'common' as default

      -

      Accessors

      • get causes(): undefined | Cause[]
      • Causes source of error

        -

        Returns undefined | Cause[]

      • get info(): undefined | Record<string, unknown>
      • Return the info object for this error

        -

        Returns undefined | Record<string, unknown>

      • get isMulti(): boolean
      • Determine if this instance is a Multi error

        -

        Returns boolean

      • get size(): number
      • Return the number of causes of this error

        -

        Returns number

      • get uuid(): string
      • Return the unique identifier associated to this instance

        -

        Returns string

      Methods

      • Process the errors thrown by Joi into the cause array

        -

        Parameters

        Returns number

        number or error that have been introduced

        -
      • Look in the nested causes of the error and return the first occurrence of a cause with the -indicated name

        -

        Parameters

        • name: string

          name of the error to search for

          -

        Returns undefined | Cause

        the cause, if there is any present with that name

        -
      • Returns a full stack of the error and causes hierarchically. The string contains the -description of the point in the code at which the Error/Crash/Multi was instantiated

        -

        Returns undefined | string

      • Check if there is any cause in the stack with the indicated name

        -

        Parameters

        • name: string

          name of the error to search for

          -

        Returns boolean

        Boolean value as the result of the search

        -
      • Remove a error from the array of causes

        -

        Returns undefined | Cause

        the cause that have been removed

        -
      • Add a new error on the array of causes

        -

        Parameters

        • error: Cause

          Cause to be added to the array of causes

          -

        Returns void

      • Return a string formatted as name:message

        -

        Returns string

      • Get the trace of this hierarchy of errors

        -

        Returns string[]

      diff --git a/docs/classes/_mdf_js_doorkeeper.DoorKeeper.html b/docs/classes/_mdf_js_doorkeeper.DoorKeeper.html deleted file mode 100644 index 1bb47b42..00000000 --- a/docs/classes/_mdf_js_doorkeeper.DoorKeeper.html +++ /dev/null @@ -1,78 +0,0 @@ -DoorKeeper | @mdf.js

      Doorkeeper is a wrapper for AJV that allows us to validate JSONs against schemas. -It also allows us to register schemas and retrieve them later.

      -

      Type Parameters

      • T = void

      Constructors

      • Creates the Doorkeeper instance to validate JSONs using AJV with formats, keywords and errors

        -

        Type Parameters

        • T = void

        Parameters

        Returns DoorKeeper<T>

      Properties

      Doorkeeper options

      -
      uuid: string = ...

      Methods

      • Try to validate an Object against the input schema or throw a ValidationError

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -

        Returns ValidatedOutput<T, K>

      • Try to validate an Object against the input schema or throw a ValidationError

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -
        • uuid: string

          unique identifier for this operation

          -

        Returns ValidatedOutput<T, K>

      • Validate an Object against the input schema and return a boolean

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -

        Returns boolean

      • Validate an Object against the input schema and return a boolean

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -
        • uuid: string

          unique identifier for this operation

          -

        Returns boolean

      • Experimental

        Return a dereferenced schema with all the $ref resolved

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to dereference

          -
        • uuid: string = ...

          unique identifier for this operation

          -

        Returns AnySchema

        A dereferenced schema with all the $ref resolved -This method is experimental and might change in the future without notice or be -removed from a future release. Use it at your own risk.

        -
      • Checks if the given data matches the specified schema.

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema to check against.

          -
        • data: any

          The data to validate.

          -

        Returns data is ValidatedOutput<T, K>

        A boolean indicating whether the data matches the schema.

        -
      • Checks if the input schema is registered

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          schema asked for

          -

        Returns boolean

          -
        • if the schema is registered in the ajv collection
        • -
        -
      • Registers a group of schemas from an object using the keys of the -object as key and the value as the validation schema

        -

        Parameters

        • schemas: Record<SchemaSelector<T>, AnySchema>

          Object containing the [key, validation schema]

          -

        Returns DoorKeeper<T>

          -
        • the instance
        • -
        -
      • Registers a group of schemas from an array and compiles them

        -

        Parameters

        • schemas: AnySchema[]

          Array containing the

          -

        Returns DoorKeeper<T>

          -
        • the instance
        • -
        -
      • Registers one schema with its key

        -

        Parameters

        • key: SchemaSelector<T>

          the key with which identify the schema

          -
        • validatorSchema: AnySchema

          the schema to be registered

          -

        Returns DoorKeeper<T>

          -
        • the instance
        • -
        -
      • Validate an Object against the input schema

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -
        • uuid: string

          unique identifier for this operation

          -
        • callback: ResultCallback<T, K>

          callback function with the result of the validation

          -

        Returns void

      • Validate an Object against the input schema

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -
        • callback: ResultCallback<T, K>

          callback function with the result of the validation

          -

        Returns void

      • Validate an Object against the input schema

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -
        • uuid: string

          unique identifier for this operation

          -

        Returns Promise<ValidatedOutput<T, K>>

      • Validate an Object against the input schema

        -

        Type Parameters

        • K extends string

        Parameters

        • schema: K

          The schema we want to validate

          -
        • data: any

          Object to be validated

          -

        Returns Promise<ValidatedOutput<T, K>>

      diff --git a/docs/classes/_mdf_js_faker.Factory.html b/docs/classes/_mdf_js_faker.Factory.html deleted file mode 100644 index b7bc3b93..00000000 --- a/docs/classes/_mdf_js_faker.Factory.html +++ /dev/null @@ -1,128 +0,0 @@ -Factory | @mdf.js

      Class Factory<T, R>

      Factory for building JavaScript objects, mostly useful for setting up test data

      -

      Type Parameters

      Constructors

      Methods

      • Register a callback function to be called after the object is generated. The callback function -receives the generated object as first argument and the resolved options as second argument.

        -

        Parameters

        • callback: ((object: T, options: R) => T)

          Callback function

          -
            • (object, options): T
            • Parameters

              • object: T
              • options: R

              Returns T

        Returns Factory<T, R>

        factory.after((user) => {
        user.name = user.name.toUpperCase();
        }); -
        - -
      • Define an attribute on this factory

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -

        Returns Factory<T, R>

        factory.attr('name');
        -
        - -
      • Define an attribute on this factory using a default value (e.g. a string or number)

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -
        • defaultValue: DefaultValue<T, K>

          Default value of attribute

          -

        Returns Factory<T, R>

        factory.attr('name', 'John Doe');
        -
        - -
      • Define an attribute on this factory using a generator function

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -
        • generator: Builder<T, K>

          Value generator function

          -

        Returns Factory<T, R>

        factory.attr('name', () => function() { return 'John Doe'; });
        -
        - -
      • Define an attribute on this factory using a generator function and dependencies on options or -other attributes

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -
        • dependencies: Dependencies<T, K>

          Array of dependencies as option or attribute names that are used by the -generator function to generate the value of this attribute

          -
        • generator: Builder<T, K>

          Value generator function. The generator function will be called with the -resolved values of the dependencies as arguments.

          -

        Returns Factory<T, R>

        factory.attr('name', ['firstName', 'lastName'], (firstName, lastName) => {
        return `${firstName} ${lastName}`;
        }); -
        - -
      • Define multiple attributes on this factory using a default value (e.g. a string or number) or -generator function. If you need to define dependencies on options or other attributes, use the -attr method instead.

        -

        Parameters

        • attributes: {
              [K in string | number | symbol]: DefaultValue<T, K> | Builder<T, K>
          }

          Object with multiple attributes

          -

        Returns Factory<T, R>

        factory.attrs({
        name: 'John Doe',
        age: function() { return 21; },
        }); -
        - -
      • Returns an object that is generated by the factory. -The optional option likelihood is a number between 0 and 100 that defines the probability -that the generated object contains wrong data. This is useful for testing if your code can -handle wrong data. The default value is 100, which means that the generated object always -contains correct data.

        -

        Parameters

        • attributes: {
              [K in string | number | symbol]?: T[K]
          } = {}

          object containing attribute override key value pairs

          -
        • options: {
              likelihood?: number;
              [key: string]: any;
          } = ...

          object containing option key value pairs

          -
          • [key: string]: any
          • Optionallikelihood?: number

        Returns T

      • Returns an array of objects that are generated by the factory. -The optional option likelihood is a number between 0 and 100 that defines the probability -that the generated object contains wrong data. This is useful for testing if your code can -handle wrong data. The default value is 100, which means that the generated object always -contains correct data.

        -

        Parameters

        • size: number

          number of objects to generate

          -
        • attributes: {
              [K in string | number | symbol]?: T[K]
          } = {}

          object containing attribute override key value pairs

          -
        • options: {
              likelihood?: number;
              [key: string]: any;
          } = ...

          object containing option key value pairs

          -
          • [key: string]: any
          • Optionallikelihood?: number

        Returns T[]

        factory.buildList(3, { name: 'John Doe' });
        -
        - -
      • Extend this factory with another factory. The attributes and options of the other factory are -merged into this factory. If an attribute or option with the same name already exists, it is -overwritten.

        -

        Type Parameters

        • P extends Partial<T>
        • J extends Partial<R>

        Parameters

        • factory: Factory<P, J>

          Factory to extend this factory with

          -

        Returns Factory<T, R>

      • Define an option for this factory using a default value. Options are values that are not -directly used in the generated object, but can be used to influence the generation process. -For example, you could define an option withAddress that, when set to true, would generate -an address and add it to the generated object. Like attributes, options can have dependencies -on other options but not on attributes.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • opt: K

          Name of option

          -
        • defaultValue: DefaultValue<R, K>

          Default value of option

          -

        Returns Factory<T, R>

        factory.option('withAddress', false);
        -
        - -
      • Define an option for this factory using a generator function. Options are values that are not -directly used in the generated object, but can be used to influence the generation process. -For example, you could define an option withAddress that, when set to true, would generate -an address and add it to the generated object. Like attributes, options can have dependencies -on other options but not on attributes.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • opt: K

          Name of option

          -
        • generator: Builder<R, K>

          Value generator function

          -

        Returns Factory<T, R>

        factory.option('withAddress', () => function() { return false; });
        -
        - -
      • Define an option for this factory using a generator function with dependencies in other -options. Options are values that are not directly used in the generated object, but can be -used to influence the generation process. For example, you could define an option -withAddress that, when set to true, would generate an address and add it to the generated -object. Like attributes, options can have dependencies on other options but not on attributes.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • opt: K

          Name of option

          -
        • dependencies: Dependencies<R, K>

          Array of dependencies as option names that are used by the generator -function to generate the value of this option

          -
        • generator: Builder<R, K>

          Value generator function with dependencies in other options. The generator -function will be called with the resolved values of the dependencies as arguments.

          -

        Returns Factory<T, R>

      • Reset all the sequences of this factory

        -

        Returns void

      • Define an auto incrementing sequence attribute of the object. Default value is 1.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -

        Returns Factory<T, R>

        factory.sequence('id');
        -
        - -
      • Define an auto incrementing sequence attribute of the object where the sequence value is -generated by a generator function that is called with the current sequence value as argument.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -
        • generator: Builder<T, K>

          Value generator function

          -

        Returns Factory<T, R>

        factory.sequence('id', (i) => function() { return i + 11; });
        -
        - -
      • Define an auto incrementing sequence attribute of the object where the sequence value is -generated by a generator function that is called with the current sequence value as argument -and dependencies on options or other attributes.

        -

        Type Parameters

        • K extends string | number | symbol

        Parameters

        • attr: K

          Name of attribute

          -
        • dependencies: (string | K)[]

          Array of dependencies as option or attribute names that are used by the -generator function to generate the value of the sequence attribute

          -
        • generator: Builder<T, K>

          Value generator function

          -

        Returns Factory<T, R>

        factory.sequence('id', ['idPrefix'], (i, idPrefix) => function() {
        return `${idPrefix}${i}`;
        }); -
        - -
      diff --git a/docs/classes/_mdf_js_firehose.Firehose.html b/docs/classes/_mdf_js_firehose.Firehose.html deleted file mode 100644 index 6dcb787b..00000000 --- a/docs/classes/_mdf_js_firehose.Firehose.html +++ /dev/null @@ -1,77 +0,0 @@ -Firehose | @mdf.js

      Class Firehose<Type, Data, CustomHeaders, CustomOptions>

      Firehose class -Allows to create a firehose(DTL pipeline) instance to manage the flow of jobs between sources and -sinks. Sinks are the final destination of the jobs, sources are the origin of the jobs and the -engine is the processing unit that manages the flow of jobs between sources and sinks applying -strategies to the jobs.

      -

      Type Parameters

      • Type extends string = string

        Job type, used as selector for strategies in job processors

        -
      • Data = any

        Job payload

        -
      • CustomHeaders extends Record<string, any> = NoMoreHeaders

        Custom headers, used to pass specific information for job processors

        -
      • CustomOptions extends Record<string, any> = NoMoreOptions

        Custom options, used to pass specific information for job processors

        -

      Hierarchy

      • EventEmitter
        • Firehose

      Implements

      Constructors

      Properties

      componentId: string = ...

      Provider unique identifier for trace purposes

      -
      name: string

      Firehose name

      -

      Accessors

      • get metrics(): Registry<"text/plain; version=0.0.4; charset=utf-8">
      • Return the metrics registry

        -

        Returns Registry<"text/plain; version=0.0.4; charset=utf-8">

      • get status(): "pass" | "fail" | "warn"
      • Overall component status

        -

        Returns "pass" | "fail" | "warn"

      Methods

      • Stop and close all the streams

        -

        Returns Promise<void>

      • Perform the restart of the firehose

        -

        Returns Promise<void>

      • Perform the piping of all the streams

        -

        Returns Promise<void>

      • Stop the active sink and source and unpipe them from the engine

        -

        Returns Promise<void>

      Events

      • Register an event listener over the done event, which is emitted when a job has ended, either -due to completion or failure.

        -

        Parameters

        Returns this

      • Removes the specified listener from the listener array for the done event.

        -

        Parameters

        Returns this

      • Add a listener for the error event, emitted when the component detects an error.

        -

        Parameters

        • event: "error"

          error event

          -
        • listener: ((error: Crash | Multi | Error) => void)

          Error event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the status event, emitted when the component status changes.

        -

        Parameters

        • event: "status"

          status event

          -
        • listener: ((status: "pass" | "fail" | "warn") => void)

          Status event listener

          -
            • (status): void
            • Parameters

              • status: "pass" | "fail" | "warn"

              Returns void

        Returns this

      • Register an event listener over the job event, which is emitted when a new job is received -from a source.

        -

        Parameters

        Returns this

      • Register an event listener over the done event, which is emitted when a job has ended, either -due to completion or failure.

        -

        Parameters

        Returns this

      • Register an event listener over the hold event, which is emitted when the engine is paused due -to inactivity.

        -

        Parameters

        • event: "hold"

          restart event

          -
        • listener: (() => void)
            • (): void
            • Returns void

        Returns this

      • Registers a one-time event listener over the done event, which is emitted when a job has -ended, either due to completion or failure.

        -

        Parameters

        Returns this

      • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a job has ended, either due to completion or failure.

        -

        Parameters

        Returns this

      • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a job has ended, either due to completion or failure.

        -

        Parameters

        Returns this

      • Removes all listeners, or those of the specified event.

        -

        Parameters

        • Optionalevent: "done"

          done event

          -

        Returns this

      • Removes the specified listener from the listener array for the done event.

        -

        Parameters

        Returns this

      diff --git a/docs/classes/_mdf_js_jsonl_archiver.ArchiverManager.html b/docs/classes/_mdf_js_jsonl_archiver.ArchiverManager.html deleted file mode 100644 index 6d13338f..00000000 --- a/docs/classes/_mdf_js_jsonl_archiver.ArchiverManager.html +++ /dev/null @@ -1,33 +0,0 @@ -ArchiverManager | @mdf.js

      Class responsible of managing jsonl file store operations

      -

      Hierarchy

      • EventEmitter
        • ArchiverManager

      Constructors

      Accessors

      Methods

      Events

      on -

      Constructors

      Accessors

      • get stats(): Record<string, FileStats>
      • Returns the statistics of the files

        -

        Returns Record<string, FileStats>

      Methods

      • Appends data to a JSONL file.

        -

        Parameters

        • data: Record<string, any> | Record<string, any>[]

          Data to append

          -

        Returns Promise<AppendResult>

      • Appends data to a JSONL file.

        -

        Parameters

        • data: Record<string, any> | Record<string, any>[]

          Data to append

          -
        • Optionalfilename: string

          Name of the file to append data to

          -

        Returns Promise<AppendResult>

      • Returns the error status of the file handlers

        -

        Returns boolean

      • Starts the ArchiverManager

        -

        Returns Promise<void>

      • Stops the jsonl file store manager

        -

        Returns Promise<void>

      Events

      • Add a listener for the error event, emitted when there is an error in a file handler -operation.

        -

        Parameters

        • event: "error"

          error event

          -
        • listener: ((error: Crash) => void)

          Error event listener

          -
            • (error): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the rotate event, emitted when a file is rotated.

        -

        Parameters

        • event: "rotate"

          error event

          -
        • listener: ((stats: FileStats) => void)

          Error event listener

          -
            • (stats): void
            • Parameters

              Returns void

        Returns this

      • Add a listener for the resolve event, emitted when an operation is resolved.

        -

        Parameters

        • event: "resolve"

          resolve event

          -
        • listener: ((stats: FileStats) => void)

          Resolve event listener

          -
            • (stats): void
            • Parameters

              Returns void

        Returns this

      • Adds a listener for the handlerCleaned event, emitted when a handler is cleaned up due to inactivity.

        -

        Parameters

        • event: "handlerCleaned"

          handlerCleaned event

          -
        • listener: ((handlerName: string) => void)

          Handler cleaned event listener

          -
            • (handlerName): void
            • Parameters

              • handlerName: string

              Returns void

        Returns this

      diff --git a/docs/classes/_mdf_js_logger.DebugLogger.html b/docs/classes/_mdf_js_logger.DebugLogger.html deleted file mode 100644 index ee4de4bc..00000000 --- a/docs/classes/_mdf_js_logger.DebugLogger.html +++ /dev/null @@ -1,55 +0,0 @@ -DebugLogger | @mdf.js

      Represents a logger instance with different log levels and functions.

      -

      Implements

      Constructors

      Properties

      Methods

      Constructors

      • Creates a new instance of the default logger

        -

        Parameters

        • name: string

          Name of the provider

          -

        Returns DebugLogger

      Properties

      debug: LoggerFunction = ...

      Log events in the DEBUG level: all the information in a detailed way. -This level used to be necessary only in the debugging process, so not all the data is -reported, only the related with the main processes and tasks.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -
      error: LoggerFunction = ...

      Log events in the ERROR level: all the errors and problems with detailed information.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -
      info: LoggerFunction = ...

      Log events in the INFO level: only relevant events are reported. -This level is the default level.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -
      silly: LoggerFunction = ...

      Log events in the SILLY level: all the information in a very detailed way. -This level used to be necessary only in the development process, and the meta data used to be -the results of the operations.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -
      stream: {
          write: ((message: string) => void);
      } = ...

      Stream logger

      -
      verbose: LoggerFunction = ...

      Log events in the VERBOSE level: trace information without details. -This level used to be necessary only in system configuration process, so information about -the settings and startup process used to be reported.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -
      warn: LoggerFunction = ...

      Log events in the WARN level: information about possible problems or dangerous situations.

      -

      human readable information to log

      -

      unique identifier for the actual job/task/request process

      -

      context (class/function) where this logger is logging

      -

      extra information

      -

      Methods

      • Log events in the ERROR level: all the information in a very detailed way. -This level used to be necessary only in the development process.

        -

        Parameters

        • rawError: Crash | Boom | Multi

          crash error instance

          -
        • Optionalcontext: string

          context (class/function) where this logger is logging

          -

        Returns void

      diff --git a/docs/classes/_mdf_js_middlewares.Audit.html b/docs/classes/_mdf_js_middlewares.Audit.html deleted file mode 100644 index 876f0946..00000000 --- a/docs/classes/_mdf_js_middlewares.Audit.html +++ /dev/null @@ -1,11 +0,0 @@ -Audit | @mdf.js

      Methods

      • Audit logger function

        -

        Parameters

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Request audit logger middleware handler

        -

        Parameters

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Audit logger middleware instance

        -

        Parameters

        Returns Audit

      diff --git a/docs/classes/_mdf_js_middlewares.AuthZ.html b/docs/classes/_mdf_js_middlewares.AuthZ.html deleted file mode 100644 index a20b5d65..00000000 --- a/docs/classes/_mdf_js_middlewares.AuthZ.html +++ /dev/null @@ -1,6 +0,0 @@ -AuthZ | @mdf.js

      AuthZ

      -

      Constructors

      Methods

      Constructors

      Methods

      • Perform the authorization based on jwt token for Socket.IO

        -

        Parameters

        • Optionaloptions: AuthZOptions

          authorization options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      diff --git a/docs/classes/_mdf_js_middlewares.BodyParser.html b/docs/classes/_mdf_js_middlewares.BodyParser.html deleted file mode 100644 index 39612b24..00000000 --- a/docs/classes/_mdf_js_middlewares.BodyParser.html +++ /dev/null @@ -1,18 +0,0 @@ -BodyParser | @mdf.js

      Constructors

      Methods

      • Returns middleware that only parses json and only looks at requests where the Content-Type -header matches the type option.

        -

        Parameters

        • Optionaloptions: OptionsJson

          json body parser options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Returns middleware that parses all bodies as a Buffer and only looks at requests where the -Content-Type header matches the type option.

        -

        Parameters

        • Optionaloptions: Options

          body parser options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Returns middleware that parses all bodies as a string and only looks at requests where the -Content-Type header matches the type option.

        -

        Parameters

        • Optionaloptions: OptionsText

          text body parser options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Returns middleware that only parses urlencoded bodies and only looks at requests where the -Content-Type header matches the type option

        -

        Parameters

        • Optionaloptions: OptionsUrlencoded

          url encoded body parser options

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      diff --git a/docs/classes/_mdf_js_middlewares.Default.html b/docs/classes/_mdf_js_middlewares.Default.html deleted file mode 100644 index 41da1688..00000000 --- a/docs/classes/_mdf_js_middlewares.Default.html +++ /dev/null @@ -1,8 +0,0 @@ -Default | @mdf.js

      Constructors

      Methods

      Constructors

      Methods

      • Format the links to be used in the default handler

        -

        Parameters

        • baseRequestUrl: string

          base url where the observability is served

          -
        • links: Links = {}

          list of links to be formatted

          -

        Returns Links

      • Request traceability middleware handler

        -

        Parameters

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      diff --git a/docs/classes/_mdf_js_middlewares.ErrorHandler.html b/docs/classes/_mdf_js_middlewares.ErrorHandler.html deleted file mode 100644 index b3c95b4b..00000000 --- a/docs/classes/_mdf_js_middlewares.ErrorHandler.html +++ /dev/null @@ -1,5 +0,0 @@ -ErrorHandler | @mdf.js

      Constructors

      Methods

      Constructors

      Methods

      • Return a error handler middleware instance

        -

        Parameters

        • Optionallogger: Logger

          logger instance

          -

        Returns ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      diff --git a/docs/classes/_mdf_js_middlewares.Logger.html b/docs/classes/_mdf_js_middlewares.Logger.html deleted file mode 100644 index c085e964..00000000 --- a/docs/classes/_mdf_js_middlewares.Logger.html +++ /dev/null @@ -1,4 +0,0 @@ -Logger | @mdf.js

      Constructors

      Methods

      Constructors

      Methods

      • Request logger middleware handler

        -

        Parameters

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      diff --git a/docs/classes/_mdf_js_middlewares.Multer.html b/docs/classes/_mdf_js_middlewares.Multer.html deleted file mode 100644 index a4a95ecf..00000000 --- a/docs/classes/_mdf_js_middlewares.Multer.html +++ /dev/null @@ -1,69 +0,0 @@ -Multer | @mdf.js

      Multer middleware wrapping for multipart/from-data

      -

      WARNING: -Make sure that you always handle the files that a user uploads. Never add multer as a global -middleware since a malicious user could upload files to a route that you didn't anticipate. Only -use this function on routes where you are handling the uploaded files.

      -

      Methods

      • Accepts all files that comes over the wire. An array of files will be stored in req.files.

        -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Accept an array of files, all with the name fieldName. Optionally error out if more than -maxCount files are uploaded. The array of files will be stored in req.files

        -

        Parameters

        • fieldName: string

          name of file

          -
        • OptionalmaxCount: number

          maximum number of files

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Accept a mix of files, specified by fields. An object with arrays of files will be stored in -req.files.

        -

        Parameters

        • fields: readonly Field[]

          array of entries

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]
        -
        - -
      • Accept only text fields. If any file upload is made, error with code "Unexpected field" will -be issued.

        -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Accept a single file with the name fieldName. The single file will be stored in req.file

        -

        Parameters

        • fieldName: string

          name of the file

          -

        Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

      • Return a new instance of the multipart/form-data middleware that accepts all files that comes -over the wire. An array of files will be stored in req.files.

        -

        Parameters

        • Optionalstorage: StorageEngine

          storage engine used for this middleware

          -
        • OptionalallowedMineTypes: string | string[]

          Allowed mime types allowed for this multer instance

          -
        • Optionallimits: {}

          Limits for the middleware

          -

          Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

        • Return a new instance of the multipart/form-data middleware that accepts an array of files, all -with the name fieldName. Optionally error out if more than maxCount files are uploaded. The -array of files will be stored in req.files

          -

          Parameters

          • fieldName: string

            name of the file field

            -
          • OptionalmaxCount: number

            maximum number of files

            -
          • Optionalstorage: StorageEngine

            storage engine used for this middleware

            -
          • OptionalallowedMineTypes: string | string[]

            Allowed mime types allowed for this multer instance

            -
          • Optionallimits: {}

            Limits for the middleware

            -

            Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

          • Return a new instance of the multipart/form-data middleware that accepts a mix of files, -specified by fields. An object with arrays of files will be stored in req.files.

            -

            Parameters

            • fields: readonly Field[]

              array of entries

              -
            • Optionalstorage: StorageEngine

              storage engine used for this middleware

              -
            • OptionalallowedMineTypes: string | string[]

              Allowed mime types allowed for this multer instance

              -
            • Optionallimits: {}

              Limits for the middleware

              -

              Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

              [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]
              -
              - -
            • Return a new instance of the multipart/form-data middleware

              -

              Parameters

              • Optionalstorage: StorageEngine

                storage engine used for this middleware

                -
              • OptionalallowedMineTypes: string | string[]

                Allowed mime types allowed for this multer instance

                -
              • Optionallimits: {}

                Limits for the middleware

                -

                Returns Multer

              • Return a new instance of the multipart/form-data middleware that accepts only text fields. If -any file upload is made, error with code "Unexpected field" will be issued.

                -

                Parameters

                • Optionalstorage: StorageEngine

                  storage engine used for this middleware

                  -
                • OptionalallowedMineTypes: string | string[]

                  Allowed mime types allowed for this multer instance

                  -
                • Optionallimits: {}

                  Limits for the middleware

                  -

                  Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

                • Return a new instance of the multipart/form-data middleware that accepts a single file with the -name fieldName. The single file will be stored in req.file

                  -

                  Parameters

                  • fieldName: string

                    name of the file field

                    -
                  • Optionalstorage: StorageEngine

                    storage engine used for this middleware

                    -
                  • OptionalallowedMineTypes: string | string[]

                    Allowed mime types allowed for this multer instance

                    -
                  • Optionallimits: {}

                    Limits for the middleware

                    -

                    Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

                  diff --git a/docs/classes/_mdf_js_middlewares.NoCache.html b/docs/classes/_mdf_js_middlewares.NoCache.html deleted file mode 100644 index 19eab823..00000000 --- a/docs/classes/_mdf_js_middlewares.NoCache.html +++ /dev/null @@ -1,4 +0,0 @@ -NoCache | @mdf.js

                  Constructors

                  Methods

                  Constructors

                  Methods

                  • Request cache middleware handler

                    -

                    Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

                  diff --git a/docs/classes/_mdf_js_middlewares.RequestId.html b/docs/classes/_mdf_js_middlewares.RequestId.html deleted file mode 100644 index ff2a7ead..00000000 --- a/docs/classes/_mdf_js_middlewares.RequestId.html +++ /dev/null @@ -1,4 +0,0 @@ -RequestId | @mdf.js

                  Constructors

                  Methods

                  Constructors

                  Methods

                  • Request traceability middleware handler

                    -

                    Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>

                  diff --git a/docs/classes/_mdf_js_middlewares.Security.html b/docs/classes/_mdf_js_middlewares.Security.html deleted file mode 100644 index 27ea4c9c..00000000 --- a/docs/classes/_mdf_js_middlewares.Security.html +++ /dev/null @@ -1,5 +0,0 @@ -Security | @mdf.js

                  Constructors

                  Methods

                  Constructors

                  Methods

                  • Return an array of security middlewares

                    -

                    Parameters

                    • enable: boolean = true

                      flag to enable or disable the security middleware

                      -

                    Returns RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>[]

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyConsumerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyConsumerAdapter.html deleted file mode 100644 index 3e11388c..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyConsumerAdapter.html +++ /dev/null @@ -1,24 +0,0 @@ -DummyConsumerAdapter | @mdf.js

                  Hierarchy

                  • DummyAdapter
                    • DummyConsumerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  • Create a new OpenC2 adapter for Dummy

                    -

                    Parameters

                    • adapterOptions: AdapterOptions

                      Adapter configuration options

                      -

                    Returns DummyConsumerAdapter

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter from the underlayer transport system

                    -

                    Returns Promise<void>

                  • Subscribe the incoming message handler to the underlayer transport system

                    -

                    Parameters

                    • handler: any

                      handler to be used

                      -

                    Returns Promise<void>

                  • Unsubscribe the incoming message handler from the underlayer transport system

                    -

                    Parameters

                    • handler: any

                      handler to be used

                      -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyProducerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyProducerAdapter.html deleted file mode 100644 index 2e08b904..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.Dummy.DummyProducerAdapter.html +++ /dev/null @@ -1,21 +0,0 @@ -DummyProducerAdapter | @mdf.js

                  Hierarchy

                  • DummyAdapter
                    • DummyProducerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  • Create a new OpenC2 adapter for Dummy

                    -

                    Parameters

                    • adapterOptions: AdapterOptions

                      Adapter configuration options

                      -

                    Returns DummyProducerAdapter

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter from the underlayer transport system

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisConsumerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisConsumerAdapter.html deleted file mode 100644 index ac46e166..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisConsumerAdapter.html +++ /dev/null @@ -1,25 +0,0 @@ -RedisConsumerAdapter | @mdf.js

                  Hierarchy

                  • RedisAdapter
                    • RedisConsumerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  • Create a new OpenC2 adapter for Redis

                    -

                    Parameters

                    • adapterOptions: AdapterOptions

                      Adapter configuration options

                      -
                    • OptionalredisOptions: Redis.Config

                      Redis configuration options

                      -

                    Returns RedisConsumerAdapter

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Subscribe the incoming message handler to the underlayer transport system

                    -

                    Parameters

                    Returns Promise<void>

                  • Unsubscribe the incoming message handler from the underlayer transport system

                    -

                    Parameters

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisProducerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisProducerAdapter.html deleted file mode 100644 index 48c4333c..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.Redis.RedisProducerAdapter.html +++ /dev/null @@ -1,22 +0,0 @@ -RedisProducerAdapter | @mdf.js

                  Hierarchy

                  • RedisAdapter
                    • RedisProducerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  • Create a new OpenC2 adapter for Redis

                    -

                    Parameters

                    • adapterOptions: AdapterOptions

                      Adapter configuration options

                      -
                    • OptionalredisOptions: Redis.Config

                      Redis configuration options

                      -

                    Returns RedisProducerAdapter

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html deleted file mode 100644 index e138a81c..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOConsumerAdapter.html +++ /dev/null @@ -1,25 +0,0 @@ -SocketIOConsumerAdapter | @mdf.js

                  Hierarchy

                  • SocketIOAdapter
                    • SocketIOConsumerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Subscribe the incoming message handler to the underlayer transport system

                    -

                    Parameters

                    Returns Promise<void>

                  • Unsubscribe the incoming message handler from the underlayer transport system

                    -

                    Parameters

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html b/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html deleted file mode 100644 index e73e1494..00000000 --- a/docs/classes/_mdf_js_openc2.Adapters.SocketIO.SocketIOProducerAdapter.html +++ /dev/null @@ -1,22 +0,0 @@ -SocketIOProducerAdapter | @mdf.js

                  Hierarchy

                  • SocketIOAdapter
                    • SocketIOProducerAdapter

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get checks(): Checks<any>
                  • Component checks

                    -

                    Returns Checks<any>

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get status(): "pass" | "fail" | "warn"
                  • Adapter health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2.Factory.Consumer.html b/docs/classes/_mdf_js_openc2.Factory.Consumer.html deleted file mode 100644 index 82866205..00000000 --- a/docs/classes/_mdf_js_openc2.Factory.Consumer.html +++ /dev/null @@ -1,13 +0,0 @@ -Consumer | @mdf.js

                  Constructors

                  Methods

                  Constructors

                  Methods

                  • Create an instance of OpenC2 Consumer with Dummy interface

                    -

                    Parameters

                    Returns Consumer

                  • Create an instance of OpenC2 Consumer with Redis interface

                    -

                    Parameters

                    Returns Consumer

                  • Create an instance of OpenC2 Consumer with SocketIO interface

                    -

                    Parameters

                    Returns Consumer

                  diff --git a/docs/classes/_mdf_js_openc2.Factory.GatewayFactory.html b/docs/classes/_mdf_js_openc2.Factory.GatewayFactory.html deleted file mode 100644 index bede89de..00000000 --- a/docs/classes/_mdf_js_openc2.Factory.GatewayFactory.html +++ /dev/null @@ -1,12 +0,0 @@ -GatewayFactory | @mdf.js

                  Constructors

                  Methods

                  • Create an instance of OpenC2 Gateway, Redis (Consumer) to WebSocketIO (Producer)

                    -

                    Parameters

                    Returns Gateway

                  • Create an instance of OpenC2 Gateway, WebSocketIO (Consumer) to Redis (Producer)

                    -

                    Parameters

                    Returns Gateway

                  diff --git a/docs/classes/_mdf_js_openc2.Factory.Producer.html b/docs/classes/_mdf_js_openc2.Factory.Producer.html deleted file mode 100644 index 68a14e24..00000000 --- a/docs/classes/_mdf_js_openc2.Factory.Producer.html +++ /dev/null @@ -1,13 +0,0 @@ -Producer | @mdf.js

                  Constructors

                  Methods

                  Constructors

                  Methods

                  • Create an instance of OpenC2 Producer with Dummy interface

                    -

                    Parameters

                    Returns Producer

                  • Create an instance of OpenC2 Producer with Redis interface

                    -

                    Parameters

                    Returns Producer

                  • Create an instance of OpenC2 Producer with SocketIO interface

                    -

                    Parameters

                    Returns Producer

                  diff --git a/docs/classes/_mdf_js_openc2.ServiceBus.html b/docs/classes/_mdf_js_openc2.ServiceBus.html deleted file mode 100644 index 8bc53b80..00000000 --- a/docs/classes/_mdf_js_openc2.ServiceBus.html +++ /dev/null @@ -1,31 +0,0 @@ -ServiceBus | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, -besides the error handling and identity, it has a start, stop and close methods to manage the -resource lifecycle. It also has a checks property to define the checks that will be performed -over the resource to achieve the resulted status. -The most typical example of a resource are the Provider that allow to access to external -databases, message brokers, etc.

                  -

                  Hierarchy

                  • EventEmitter
                    • ServiceBus

                  Implements

                  Constructors

                  Properties

                  Accessors

                  Methods

                  Constructors

                  Properties

                  componentId: string = ...

                  Component identification

                  -
                  name: string

                  name of the service bus

                  -

                  Accessors

                  • get status(): "pass" | "fail" | "warn"
                  • Return the status of the server

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the server and disconnect all the actual connections

                    -

                    Returns Promise<void>

                  • Emitted when a server operation has some problem

                    -

                    Parameters

                    • event: "error"
                    • listener: ((error: Crash | Error) => void)
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Emitted on every state change

                    -

                    Parameters

                    • event: "status"
                    • listener: ((status: "pass" | "fail" | "warn") => void)
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Start the underlayer Socket.IO server

                    -

                    Returns Promise<void>

                  • Close the server and disconnect all the actual connections

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_openc2_core.Accessors.html b/docs/classes/_mdf_js_openc2_core.Accessors.html deleted file mode 100644 index 6c937eb9..00000000 --- a/docs/classes/_mdf_js_openc2_core.Accessors.html +++ /dev/null @@ -1,32 +0,0 @@ -Accessors | @mdf.js

                  Constructors

                  Methods

                  • Return the action of the actual command

                    -

                    Parameters

                    • command: Command

                      command to be processed

                      -

                    Returns Action

                  • Return the action of the actual message

                    -

                    Parameters

                    Returns Action

                  • Return the a property from actuators in the command

                    -

                    Parameters

                    • command: Command

                      command to be processed

                      -
                    • profile: string

                      actuator profile to find

                      -

                    Returns any

                  • Return the actuators in the command

                    -

                    Parameters

                    • command: Command

                      command to be processed

                      -

                    Returns string[]

                  • Return the actuators in the command message

                    -

                    Parameters

                    Returns string[]

                  • Return the delay allowed from command

                    -

                    Parameters

                    • command: Command

                      message to be processed

                      -

                    Returns number

                  • Return the delay allowed from command message

                    -

                    Parameters

                    Returns number

                  • Convert consumer status to Subcomponent status

                    -

                    Parameters

                    Returns "pass" | "fail" | "warn"

                  • Return the target of the actual command

                    -

                    Parameters

                    • command: Command

                      command to be processed

                      -

                    Returns string

                  • Return the target of the actual message

                    -

                    Parameters

                    Returns string

                  diff --git a/docs/classes/_mdf_js_openc2_core.Consumer.html b/docs/classes/_mdf_js_openc2_core.Consumer.html deleted file mode 100644 index e6276616..00000000 --- a/docs/classes/_mdf_js_openc2_core.Consumer.html +++ /dev/null @@ -1,91 +0,0 @@ -Consumer | @mdf.js

                  Hierarchy

                  Constructors

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get actuator(): undefined | string[]
                  • Consumer actuators

                    -

                    Returns undefined | string[]

                  • set actuator(value): void
                  • Parameters

                    • value: undefined | string[]

                    Returns void

                  • Return links offered by this service

                    -

                    Returns Links

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get profiles(): undefined | string[]
                  • Consumer profiles

                    -

                    Returns undefined | string[]

                  • set profiles(value): void
                  • Parameters

                    • value: undefined | string[]

                    Returns void

                  • get router(): Router
                  • Return an Express router with access to OpenC2 information

                    -

                    Returns Router

                  • get status(): "pass" | "fail" | "warn"
                  • Component health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the OpenC2 component

                    -

                    Returns Promise<void>

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "error"

                      error event

                      -

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "command"

                      command event

                      -

                    Returns this

                  • Connect the OpenC2 Adapter to the underlayer transport system and perform the startup of the -component

                    -

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter to the underlayer transport system and perform the shutdown of -the component

                    -

                    Returns Promise<void>

                  Events

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the command event, emitted when a new command is received

                    -

                    Parameters

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the command event.

                    -

                    Parameters

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the command event, emitted when a new command is received

                    -

                    Parameters

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the command event, emitted when a new command is received. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the command event.

                    -

                    Parameters

                    Returns this

                  diff --git a/docs/classes/_mdf_js_openc2_core.Gateway.html b/docs/classes/_mdf_js_openc2_core.Gateway.html deleted file mode 100644 index 63caf5de..00000000 --- a/docs/classes/_mdf_js_openc2_core.Gateway.html +++ /dev/null @@ -1,77 +0,0 @@ -Gateway | @mdf.js

                  A service is a special kind of resource that besides Resource properties, it could offer:

                  -
                    -
                  • Its own REST API endpoints, using an express router, to expose details about service, this -endpoints will be exposed under the observability paths.
                  • -
                  • A links property to define the endpoints that the service expose, this information will be -exposed in the observability paths.
                  • -
                  • A metrics property to expose the metrics of the service, this registry will be merged with the -global metrics registry.
                  • -
                  -

                  Hierarchy

                  • EventEmitter
                    • Gateway

                  Implements

                  Constructors

                  • Regular OpenC2 gateway implementation.

                    -

                    Parameters

                    Returns Gateway

                  Properties

                  componentId: string = ...

                  Component identification

                  -

                  Accessors

                  • get consumerMap(): ConsumerMap
                  • Consumer Map

                    -

                    Returns ConsumerMap

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get router(): Router
                  • Return an Express router with access to errors registry

                    -

                    Returns Router

                  • get status(): "pass" | "fail" | "warn"
                  • Component status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the OpenC2 gateway

                    -

                    Returns Promise<void>

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "error"

                      error event

                      -

                    Returns this

                  • Connect the OpenC2 underlayer component and perform the startup of the component

                    -

                    Returns Promise<void>

                  • Disconnect the OpenC2 underlayer component and perform the startup of the component

                    -

                    Returns Promise<void>

                  Events

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  diff --git a/docs/classes/_mdf_js_openc2_core.Producer.html b/docs/classes/_mdf_js_openc2_core.Producer.html deleted file mode 100644 index dc447016..00000000 --- a/docs/classes/_mdf_js_openc2_core.Producer.html +++ /dev/null @@ -1,84 +0,0 @@ -Producer | @mdf.js

                  Hierarchy

                  Constructors

                  Properties

                  componentId: string = ...

                  Component identification

                  -
                  consumerMap: ConsumerMap

                  Consumer Map

                  -

                  Accessors

                  • Return links offered by this service

                    -

                    Returns Links

                  • get name(): string
                  • Component name

                    -

                    Returns string

                  • get router(): Router
                  • Return an Express router with access to OpenC2 information

                    -

                    Returns Router

                  • get status(): "pass" | "fail" | "warn"
                  • Component health status

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Close the OpenC2 component

                    -

                    Returns Promise<void>

                  • Issue a new command to the requested consumers. If '' is indicated as a consumer, the command -will be broadcasted. If an actuator is indicated in the command the command will not be -broadcasted even if it include the '' symbol.

                    -

                    Parameters

                    Returns Promise<ResponseMessage[]>

                  • Issue a new command to the requested consumers. If '' is indicated as a consumer, the command -will be broadcasted. If an actuator is indicated in the command the command will not be -broadcasted even if it include the '' symbol.

                    -

                    Parameters

                    • to: string[]

                      Consumer objetive of this command

                      -
                    • content: Command

                      Command to be issued

                      -

                    Returns Promise<ResponseMessage[]>

                  • Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command -will be broadcasted.

                    -

                    Parameters

                    • to: string[]

                      Consumer objetive of this command

                      -
                    • action: Action

                      command action

                      -
                    • target: Target

                      command target

                      -

                    Returns Promise<ResponseMessage[]>

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "error"

                      error event

                      -

                    Returns this

                  • Connect the OpenC2 Adapter to the underlayer transport system and perform the startup of the -component

                    -

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter to the underlayer transport system and perform the shutdown of -the component

                    -

                    Returns Promise<void>

                  Events

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component changes its status. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the error event.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the status event.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  diff --git a/docs/classes/_mdf_js_service_registry.ServiceRegistry.html b/docs/classes/_mdf_js_service_registry.ServiceRegistry.html deleted file mode 100644 index f268e824..00000000 --- a/docs/classes/_mdf_js_service_registry.ServiceRegistry.html +++ /dev/null @@ -1,64 +0,0 @@ -ServiceRegistry | @mdf.js

                  Class ServiceRegistry<CustomSettings>

                  Type Parameters

                  Hierarchy

                  • EventEmitter
                    • ServiceRegistry

                  Constructors

                  • Create a new instance of the Service Registry

                    -

                    Type Parameters

                    • CustomSettings extends Record<string, any> = Record<string, any>

                    Parameters

                    • OptionalbootstrapOptions: BootstrapOptions

                      Bootstrap settings, define how the Custom and the Service Registry -settings should be loaded.

                      -
                    • OptionalserviceRegistryOptions: ServiceRegistryOptions<CustomSettings>

                      Service Registry settings, used as a base for the Service -Registry configuration manager.

                      -
                    • OptionalcustomSettings: Partial<CustomSettings>

                      Custom settings provided by the user, used as a base for the Custom -configuration manager.

                      -

                    Returns ServiceRegistry<CustomSettings>

                  Accessors

                  • get health(): Health
                  • Returns Health

                    Service Register health information

                    -
                  • get status(): "pass" | "fail" | "warn"
                  • Returns "pass" | "fail" | "warn"

                    Service Register status

                    -

                  Methods

                  • Gets the value at path of object. If the resolved value is undefined, the defaultValue is -returned in its place.

                    -

                    Type Parameters

                    • T

                      Type of the property to return

                      -

                    Parameters

                    • path: string | string[]

                      path to the property to get

                      -
                    • OptionaldefaultValue: T

                      default value to return if the property is not found

                      -

                    Returns undefined | T

                  • Gets the value at path of object. If the resolved value is undefined, the defaultValue is -returned in its place.

                    -

                    Type Parameters

                    • P extends string | number | symbol

                    Parameters

                    • key: P

                      path to the property to get

                      -
                    • OptionaldefaultValue: CustomSettings[P]

                      default value to return if the property is not found

                      -

                    Returns undefined | CustomSettings[P]

                  • Register a resource within the service observability

                    -

                    Parameters

                    Returns void

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "command"

                      command event

                      -

                    Returns this

                  • Perform the initialization of all the service resources that has been attached

                    -

                    Returns Promise<void>

                  • Perform the stop of all the service resources that has been attached

                    -

                    Returns Promise<void>

                  Events

                  • Add a listener for the command event, emitted when a new command is received

                    -

                    Parameters

                    Returns this

                  • Removes the specified listener from the listener array for the command event.

                    -

                    Parameters

                    Returns this

                  • Add a listener for the command event, emitted when a new command is received

                    -

                    Parameters

                    Returns this

                  • Add a listener for the command event, emitted when a new command is received. This is a -one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    Returns this

                  • Removes the specified listener from the listener array for the command event.

                    -

                    Parameters

                    Returns this

                  diff --git a/docs/classes/_mdf_js_service_setup_provider.ConfigManager.html b/docs/classes/_mdf_js_service_setup_provider.ConfigManager.html deleted file mode 100644 index 2d9051b2..00000000 --- a/docs/classes/_mdf_js_service_setup_provider.ConfigManager.html +++ /dev/null @@ -1,34 +0,0 @@ -ConfigManager | @mdf.js

                  Class responsible of file management, both configuration file as validator files

                  -

                  Type Parameters

                  • SystemConfig extends Record<string, any> = Record<string, any>

                  Constructors

                  Properties

                  config: SystemConfig

                  Final configuration

                  -
                  defaultConfig: Partial<SystemConfig> = {}

                  Default configuration

                  -
                  envConfig: Partial<SystemConfig> = {}

                  Environment configuration

                  -
                  nonDisclosureConfig: Partial<SystemConfig>

                  Final configuration without environment variables

                  -
                  presets: Record<string, Partial<SystemConfig>> = {}

                  Presets configuration map

                  -

                  Accessors

                  • get error(): undefined | Multi
                  • Validation error, if exist

                    -

                    Returns undefined | Multi

                  • get isErrored(): boolean
                  • Flag to indicate that the final configuration has some errors

                    -

                    Returns boolean

                  • get preset(): undefined | string
                  • Return the preset used to create the final configuration

                    -

                    Returns undefined | string

                  • get schema(): undefined | string
                  • Return the schema used to validate the final configuration

                    -

                    Returns undefined | string

                  Methods

                  • Gets the value at path of object. If the resolved value is undefined, the defaultValue is -returned in its place.

                    -

                    Type Parameters

                    • T

                      Type of the property to return

                      -

                    Parameters

                    • path: string | string[]

                      path to the property to get

                      -
                    • OptionaldefaultValue: any

                      default value to return if the property is not found

                      -

                    Returns undefined | T

                  • Gets the value at path of object. If the resolved value is undefined, the defaultValue is -returned in its place.

                    -

                    Type Parameters

                    • P extends string | number | symbol

                    Parameters

                    • key: P

                      path to the property to get

                      -
                    • OptionaldefaultValue: any

                      default value to return if the property is not found

                      -

                    Returns undefined | SystemConfig[P]

                  diff --git a/docs/classes/_mdf_js_tasks.Group.html b/docs/classes/_mdf_js_tasks.Group.html deleted file mode 100644 index 1216c40f..00000000 --- a/docs/classes/_mdf_js_tasks.Group.html +++ /dev/null @@ -1,50 +0,0 @@ -Group | @mdf.js

                  Class Group<T, U>

                  Represents the task handler

                  -

                  Type Parameters

                  • T
                  • U

                  Hierarchy (view full)

                  Constructors

                  • Create a new task handler for a group of tasks

                    -

                    Type Parameters

                    • T
                    • U

                    Parameters

                    • tasks: TaskHandler<T, U>[]

                      The tasks to execute

                      -
                    • Optionaloptions: TaskOptions<U>

                      The options for the task

                      -
                    • OptionalatLeastOne: boolean

                      If at least one task must succeed to consider the group as successful -execution, in other case, all the tasks must succeed

                      -

                    Returns Group<T, U>

                  Properties

                  createdAt: Date

                  Date when the task was created

                  -
                  priority: number

                  Task priority

                  -
                  taskId: string

                  Task identifier, defined by the user

                  -
                  uuid: string

                  Unique task identification, unique for each task

                  -
                  weight: number

                  Task weight

                  -

                  Accessors

                  • get metadata(): MetaData
                  • Return the metadata of the task

                    -

                    Returns MetaData

                  Methods

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Cancel the task

                    -

                    Parameters

                    Returns void

                  • Execute the task

                    -

                    Returns Promise<(null | T)[]>

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<(null | T)[]>

                      The listener function to remove

                      -

                    Returns this

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, which is emitted when a task has -ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<(null | T)[]>

                      The listener function to remove

                      -

                    Returns this

                  diff --git a/docs/classes/_mdf_js_tasks.Limiter.html b/docs/classes/_mdf_js_tasks.Limiter.html deleted file mode 100644 index 65b6b5dc..00000000 --- a/docs/classes/_mdf_js_tasks.Limiter.html +++ /dev/null @@ -1,121 +0,0 @@ -Limiter | @mdf.js

                  A limiter is a queue system that allows you to control the rate of job processing. It can be used -to limit the number of concurrent jobs, the delay between each job, the maximum number of jobs in -the queue, and the strategy to use when the queue length reaches highWater.

                  -

                  Hierarchy

                  • LimiterStateHandler
                    • Limiter

                  Constructors

                  • Create a new instance of Limiter

                    -

                    Parameters

                    Returns Limiter

                  Accessors

                  • get options(): Readonly<ConsolidatedLimiterOptions>
                  • Returns the limiter options

                    -

                    Returns Readonly<ConsolidatedLimiterOptions>

                  • get pending(): number
                  • Returns the number of pending jobs

                    -

                    Returns number

                  • get size(): number
                  • Returns the number of jobs in the queue

                    -

                    Returns number

                  Methods

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Register an event listener over the refill event, which is emitted when queue bucket is refilled

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Register an event listener over the seed event, which is emitted when queue is empty and a -new task is added

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Clears the queue

                    -

                    Returns void

                  • Executes a task and returns a promise that resolves when the task is done

                    -

                    Type Parameters

                    • T
                    • U

                    Parameters

                    Returns Promise<T>

                    A promise that resolves when the task is done

                    -
                  • Executes a task and returns a promise that resolves when the task is done

                    -

                    Type Parameters

                    • T
                    • U

                    Parameters

                    Returns Promise<T>

                    A promise that resolves when the task is done

                    -
                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: string

                      done event

                      -
                    • listener: DoneListener

                      The listener function to remove

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the refill event.

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to remove

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the seed event.

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to remove

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Register an event listener over the refill event, which is emitted when queue bucket is refilled

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Register an event listener over the seed event, which is emitted when queue is empty and a -new task is added

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Registers a one-time event listener over the done event, which is emitted when a task has -ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the refill event, which is emitted when queue bucket -is refilled

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Registers a one-time event listener over the seed event, which is emitted when queue is empty -and a new task is added

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Pipes the limiter to another limiter

                    -

                    Parameters

                    Returns void

                  • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a event listener over the refill event, at the beginning of the listeners array, -which is emitted when queue bucket is refilled

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Registers a event listener over the seed event, at the beginning of the listeners array, which -is emitted when queue is empty and a new task is added

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the refill event, at the beginning of the listeners -array, which is emitted when queue bucket is refilled

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Registers a one-time event listener over the seed event, at the beginning of the listeners -array, which is emitted when queue is empty and a new task is added

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to add

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "refill"

                      refill event

                      -

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "seed"

                      seed event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: string

                      done event

                      -
                    • listener: DoneListener

                      The listener function to remove

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the refill event.

                    -

                    Parameters

                    • event: "refill"

                      refill event

                      -
                    • listener: (() => void)

                      The listener function to remove

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Removes the specified listener from the listener array for the seed event.

                    -

                    Parameters

                    • event: "seed"

                      seed event

                      -
                    • listener: (() => void)

                      The listener function to remove

                      -
                        • (): void
                        • Returns void

                    Returns this

                  • Schedules a task to be executed by the limiter

                    -

                    Type Parameters

                    • T
                    • U

                    Parameters

                    Returns undefined | string

                    The task handler

                    -
                  • Schedules a task to be executed by the limiter

                    -

                    Type Parameters

                    • T
                    • U

                    Parameters

                    Returns undefined | string

                    The task handler

                    -
                  • Starts the limiter

                    -

                    Returns void

                  • Stops the limiter

                    -

                    Returns void

                  • Waits until the queue is empty

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_tasks.PollingExecutor.html b/docs/classes/_mdf_js_tasks.PollingExecutor.html deleted file mode 100644 index 1270fdbe..00000000 --- a/docs/classes/_mdf_js_tasks.PollingExecutor.html +++ /dev/null @@ -1,17 +0,0 @@ -PollingExecutor | @mdf.js

                  Polling manager

                  -

                  Hierarchy

                  • EventEmitter
                    • PollingExecutor

                  Constructors

                  Accessors

                  Methods

                  Constructors

                  Accessors

                  • get check(): Check<any>
                  • Return the stats of the polling manager

                    -

                    Returns Check<any>

                  Methods

                  • Emitted on every error

                    -

                    Parameters

                    • event: "error"
                    • listener: ((error: Crash | Multi) => void)
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Emitted on every state change

                    -

                    Parameters

                    • event: "status"
                    • listener: ((status: "pass" | "fail" | "warn") => void)
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Emitted when a task has ended

                    -

                    Parameters

                    • event: "done"
                    • listener: ((uuid: string, result: any, meta: MetaData, error?: Crash | Multi) => void)
                        • (uuid, result, meta, error?): void
                        • Parameters

                          Returns void

                    Returns this

                  • Start the polling manager

                    -

                    Returns void

                  • Stop the polling manager

                    -

                    Returns void

                  diff --git a/docs/classes/_mdf_js_tasks.Scheduler.html b/docs/classes/_mdf_js_tasks.Scheduler.html deleted file mode 100644 index c80e4521..00000000 --- a/docs/classes/_mdf_js_tasks.Scheduler.html +++ /dev/null @@ -1,71 +0,0 @@ -Scheduler | @mdf.js

                  Class Scheduler<Result, Binding, PollingGroups>

                  A scheduler is a service that manages the execution of tasks in a controlled and efficient way. -It is responsible for managing the resources and the rate limits of the tasks, and for emitting -events when the tasks are done or when an error occurs.

                  -

                  Type Parameters

                  Hierarchy

                  • EventEmitter
                    • Scheduler

                  Implements

                  Constructors

                  Properties

                  componentId: string = ...

                  Provider unique identifier for trace purposes

                  -
                  metrics: Registry<"text/plain; version=0.0.4; charset=utf-8"> = ...

                  Metrics registry

                  -
                  metricsDefinitions: MetricsDefinitions = ...

                  Metrics definitions

                  -
                  name: string

                  The name of the scheduler

                  -

                  Accessors

                  • get status(): "pass" | "fail" | "warn"
                  • Get the health status for the scheduler

                    -

                    Returns "pass" | "fail" | "warn"

                  Methods

                  • Add a listener for the done event, emitted when a task is done, with the result or the error.

                    -

                    Parameters

                    Returns this

                  • Cleanup the scheduler

                    -

                    Returns void

                  • Close the scheduler

                    -

                    Returns Promise<void>

                  • Drop a resource from the scheduler

                    -

                    Parameters

                    • resource: string

                      The resource to drop

                      -

                    Returns void

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    Returns this

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Crash | Error | Multi) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component status changes.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  • Add a listener for the done event, emitted when a task is done, with the result or the error.

                    -

                    Parameters

                    Returns this

                  • Add a listener for the done event, emitted when a task is done, with the result or the error. -This is a one-time event, the listener will be removed after the first emission.

                    -

                    Parameters

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    Returns this

                  • Start the scheduler

                    -

                    Returns Promise<void>

                  • Stop the scheduler

                    -

                    Returns Promise<void>

                  diff --git a/docs/classes/_mdf_js_tasks.Sequence.html b/docs/classes/_mdf_js_tasks.Sequence.html deleted file mode 100644 index 9d7e2e05..00000000 --- a/docs/classes/_mdf_js_tasks.Sequence.html +++ /dev/null @@ -1,48 +0,0 @@ -Sequence | @mdf.js

                  Class Sequence<T, U>

                  Represents the task handler

                  -

                  Type Parameters

                  • T
                  • U

                  Hierarchy (view full)

                  Constructors

                  Properties

                  createdAt: Date

                  Date when the task was created

                  -
                  priority: number

                  Task priority

                  -
                  taskId: string

                  Task identifier, defined by the user

                  -
                  uuid: string

                  Unique task identification, unique for each task

                  -
                  weight: number

                  Task weight

                  -

                  Accessors

                  • get metadata(): MetaData
                  • Return the metadata of the task

                    -

                    Returns MetaData

                  Methods

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Cancel the task

                    -

                    Parameters

                    Returns void

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<T>

                      The listener function to remove

                      -

                    Returns this

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, which is emitted when a task has -ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<T>

                      The listener function to remove

                      -

                    Returns this

                  diff --git a/docs/classes/_mdf_js_tasks.Single.html b/docs/classes/_mdf_js_tasks.Single.html deleted file mode 100644 index 2d9816f8..00000000 --- a/docs/classes/_mdf_js_tasks.Single.html +++ /dev/null @@ -1,52 +0,0 @@ -Single | @mdf.js

                  Class Single<T, U>

                  Represents the task handler

                  -

                  Type Parameters

                  • T
                  • U

                  Hierarchy (view full)

                  Constructors

                  Properties

                  createdAt: Date

                  Date when the task was created

                  -
                  priority: number

                  Task priority

                  -
                  taskId: string

                  Task identifier, defined by the user

                  -
                  uuid: string

                  Unique task identification, unique for each task

                  -
                  weight: number

                  Task weight

                  -

                  Accessors

                  • get metadata(): MetaData
                  • Return the metadata of the task

                    -

                    Returns MetaData

                  Methods

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Cancel the task

                    -

                    Parameters

                    Returns void

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<T>

                      The listener function to remove

                      -

                    Returns this

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, which is emitted when a task has -ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    • event: "done"

                      done event

                      -
                    • listener: DoneListener<T>

                      The listener function to remove

                      -

                    Returns this

                  diff --git a/docs/classes/_mdf_js_tasks.TaskHandler.html b/docs/classes/_mdf_js_tasks.TaskHandler.html deleted file mode 100644 index 217aef70..00000000 --- a/docs/classes/_mdf_js_tasks.TaskHandler.html +++ /dev/null @@ -1,47 +0,0 @@ -TaskHandler | @mdf.js

                  Class TaskHandler<Result, Binded>Abstract

                  Represents the task handler

                  -

                  Type Parameters

                  • Result
                  • Binded

                  Hierarchy (view full)

                  Constructors

                  Properties

                  createdAt: Date

                  Date when the task was created

                  -
                  priority: number

                  Task priority

                  -
                  taskId: string

                  Task identifier, defined by the user

                  -
                  uuid: string

                  Unique task identification, unique for each task

                  -
                  weight: number

                  Task weight

                  -

                  Accessors

                  Methods

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Cancel the task

                    -

                    Parameters

                    Returns void

                  • Execute the task

                    -

                    Returns Promise<Result>

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    Returns this

                  • Register an event listener over the done event, which is emitted when a task has ended, either -due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, which is emitted when a task has -ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a event listener over the done event, at the beginning of the listeners array, -which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Registers a one-time event listener over the done event, at the beginning of the listeners -array, which is emitted when a task has ended, either due to completion or failure.

                    -

                    Parameters

                    Returns this

                  • Removes all listeners, or those of the specified event.

                    -

                    Parameters

                    • Optionalevent: "done"

                      done event

                      -

                    Returns this

                  • Removes the specified listener from the listener array for the done event.

                    -

                    Parameters

                    Returns this

                  diff --git a/docs/enums/_mdf_js_core.Health.STATUS.html b/docs/enums/_mdf.js_core.Health.STATUS.html similarity index 58% rename from docs/enums/_mdf_js_core.Health.STATUS.html rename to docs/enums/_mdf.js_core.Health.STATUS.html index a0bfcdc4..4367fde4 100644 --- a/docs/enums/_mdf_js_core.Health.STATUS.html +++ b/docs/enums/_mdf.js_core.Health.STATUS.html @@ -1,5 +1,5 @@ -STATUS | @mdf.js

                  Service status

                  -

                  Enumeration Members

                  Enumeration Members

                  FAIL: "fail"
                  PASS: "pass"
                  WARN: "warn"
                  +STATUS | @mdf.js

                  Service status

                  +

                  Enumeration Members

                  Enumeration Members

                  FAIL: "fail"
                  PASS: "pass"
                  WARN: "warn"
                  diff --git a/docs/enums/_mdf_js_core.Jobs.Status.html b/docs/enums/_mdf.js_core.Jobs.Status.html similarity index 63% rename from docs/enums/_mdf_js_core.Jobs.Status.html rename to docs/enums/_mdf.js_core.Jobs.Status.html index c2defd51..730c2616 100644 --- a/docs/enums/_mdf_js_core.Jobs.Status.html +++ b/docs/enums/_mdf.js_core.Jobs.Status.html @@ -1,12 +1,12 @@ -Status | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +Status | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                  -

                  Enumeration Members

                  Enumeration Members

                  COMPLETED: "completed"

                  Job has been resolved

                  -
                  FAILED: "failed"

                  Job has been complete with failures

                  -
                  PENDING: "pending"

                  Job is pending

                  -
                  PROCESSING: "processing"

                  Job is processing

                  -
                  +

                  Enumeration Members

                  Enumeration Members

                  COMPLETED: "completed"

                  Job has been resolved

                  +
                  FAILED: "failed"

                  Job has been complete with failures

                  +
                  PENDING: "pending"

                  Job is pending

                  +
                  PROCESSING: "processing"

                  Job is processing

                  +
                  diff --git a/docs/enums/_mdf_js_core.Layer.Provider.ProviderStatus.html b/docs/enums/_mdf.js_core.Layer.Provider.ProviderStatus.html similarity index 66% rename from docs/enums/_mdf_js_core.Layer.Provider.ProviderStatus.html rename to docs/enums/_mdf.js_core.Layer.Provider.ProviderStatus.html index 78a6e0aa..85f0670d 100644 --- a/docs/enums/_mdf_js_core.Layer.Provider.ProviderStatus.html +++ b/docs/enums/_mdf.js_core.Layer.Provider.ProviderStatus.html @@ -1,5 +1,5 @@ -ProviderStatus | @mdf.js

                  Provider status

                  -

                  Enumeration Members

                  Enumeration Members

                  error: "fail"
                  running: "pass"
                  stopped: "warn"
                  +ProviderStatus | @mdf.js

                  Provider status

                  +

                  Enumeration Members

                  Enumeration Members

                  error: "fail"
                  running: "pass"
                  stopped: "warn"
                  diff --git a/docs/enums/_mdf.js_file-flinger.ErrorStrategy.html b/docs/enums/_mdf.js_file-flinger.ErrorStrategy.html new file mode 100644 index 00000000..4c9f7af3 --- /dev/null +++ b/docs/enums/_mdf.js_file-flinger.ErrorStrategy.html @@ -0,0 +1,8 @@ +ErrorStrategy | @mdf.js

                  Error strategy

                  +

                  Enumeration Members

                  Enumeration Members

                  DEAD_LETTER: "dead-letter"

                  Move the file to the dead-letter folder, emit an error and include in the skipped files

                  +
                  DELETE: "delete"

                  Delete the file, emit an error and include in the skipped files

                  +
                  IGNORE: "ignore"

                  Ignore the file, emit an error and include in the skipped files

                  +
                  diff --git a/docs/enums/_mdf.js_file-flinger.PostProcessingStrategy.html b/docs/enums/_mdf.js_file-flinger.PostProcessingStrategy.html new file mode 100644 index 00000000..80ce840e --- /dev/null +++ b/docs/enums/_mdf.js_file-flinger.PostProcessingStrategy.html @@ -0,0 +1,8 @@ +PostProcessingStrategy | @mdf.js

                  Enumeration PostProcessingStrategy

                  Post-processing strategies for errored files

                  +

                  Enumeration Members

                  Enumeration Members

                  ARCHIVE: "archive"

                  Archive the file

                  +
                  DELETE: "delete"

                  Delete the file

                  +
                  ZIP: "zip"

                  Zip the file

                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.Action.html b/docs/enums/_mdf.js_openc2-core.Control.Action.html similarity index 69% rename from docs/enums/_mdf_js_openc2_core.Control.Action.html rename to docs/enums/_mdf.js_openc2-core.Control.Action.html index 8fe9050a..44746186 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.Action.html +++ b/docs/enums/_mdf.js_openc2-core.Control.Action.html @@ -1,46 +1,46 @@ -Action | @mdf.js

                  The task or activity to be performed (i.e., the 'verb')

                  -

                  Enumeration Members

                  Allow: "allow"

                  Permit access to or execution of a Target

                  -
                  Cancel: "cancel"

                  Invalidate a previously issued Action

                  -
                  Contain: "contain"

                  Isolate a file, process, or entity so that it cannot modify or access assets or processes

                  -
                  Copy: "copy"

                  Duplicate an object, file, data flow, or artifact

                  -
                  Create: "create"

                  Add a new entity of a known type (e.g., data, files, directories)

                  -
                  Delete: "delete"

                  Remove an entity (e.g., data, files, flows)

                  -
                  Deny: "deny"

                  Prevent a certain event or action from completion, such as preventing a flow from reaching a +Action | @mdf.js

                  The task or activity to be performed (i.e., the 'verb')

                  +

                  Enumeration Members

                  Allow: "allow"

                  Permit access to or execution of a Target

                  +
                  Cancel: "cancel"

                  Invalidate a previously issued Action

                  +
                  Contain: "contain"

                  Isolate a file, process, or entity so that it cannot modify or access assets or processes

                  +
                  Copy: "copy"

                  Duplicate an object, file, data flow, or artifact

                  +
                  Create: "create"

                  Add a new entity of a known type (e.g., data, files, directories)

                  +
                  Delete: "delete"

                  Remove an entity (e.g., data, files, flows)

                  +
                  Deny: "deny"

                  Prevent a certain event or action from completion, such as preventing a flow from reaching a destination or preventing access

                  -
                  Detonate: "detonate"

                  Execute and observe the behavior of a Target (e.g., file, hyperlink) in an isolated +

                  Detonate: "detonate"

                  Execute and observe the behavior of a Target (e.g., file, hyperlink) in an isolated environment

                  -
                  Investigate: "investigate"

                  Task the recipient to aggregate and report information as it pertains to a security event or +

                  Investigate: "investigate"

                  Task the recipient to aggregate and report information as it pertains to a security event or incident

                  -
                  Locate: "locate"

                  Find an object physically, logically, functionally, or by organization

                  -
                  Query: "query"

                  Initiate a request for information

                  -
                  Redirect: "redirect"

                  Change the flow of traffic to a destination other than its original destination

                  -
                  Remediate: "remediate"

                  Task the recipient to eliminate a vulnerability or attack point

                  -
                  Restart: "restart"

                  Stop then start a system or an activity

                  -
                  Restore: "restore"

                  Return a system to a previously known state

                  -
                  Scan: "scan"

                  Systematic examination of some aspect of the entity or its environment

                  -
                  Set: "set"

                  Change a value, configuration, or state of a managed entity

                  -
                  Start: "start"

                  Initiate a process, application, system, or activity

                  -
                  Stop: "stop"

                  Halt a system or end an activity

                  -
                  Update: "update"

                  Instruct a component to retrieve, install, process, and operate in accordance with a software +

                  Locate: "locate"

                  Find an object physically, logically, functionally, or by organization

                  +
                  Query: "query"

                  Initiate a request for information

                  +
                  Redirect: "redirect"

                  Change the flow of traffic to a destination other than its original destination

                  +
                  Remediate: "remediate"

                  Task the recipient to eliminate a vulnerability or attack point

                  +
                  Restart: "restart"

                  Stop then start a system or an activity

                  +
                  Restore: "restore"

                  Return a system to a previously known state

                  +
                  Scan: "scan"

                  Systematic examination of some aspect of the entity or its environment

                  +
                  Set: "set"

                  Change a value, configuration, or state of a managed entity

                  +
                  Start: "start"

                  Initiate a process, application, system, or activity

                  +
                  Stop: "stop"

                  Halt a system or end an activity

                  +
                  Update: "update"

                  Instruct a component to retrieve, install, process, and operate in accordance with a software update, reconfiguration, or other update

                  -
                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.Features.html b/docs/enums/_mdf.js_openc2-core.Control.Features.html similarity index 60% rename from docs/enums/_mdf_js_openc2_core.Control.Features.html rename to docs/enums/_mdf.js_openc2-core.Control.Features.html index 9ae4245a..3a023467 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.Features.html +++ b/docs/enums/_mdf.js_openc2-core.Control.Features.html @@ -1,10 +1,10 @@ -Features | @mdf.js

                  Specifies the results to be returned from a query features Command

                  -

                  Enumeration Members

                  Enumeration Members

                  Pairs: "pairs"

                  List of supported Actions and applicable Targets

                  -
                  Profiles: "profiles"

                  List of profiles supported by this Actuator

                  -
                  RateLimit: "rate_limit"

                  Maximum number of Commands per minute supported by design or policy

                  -
                  Versions: "versions"

                  List of command and control Language versions supported by this Actuator

                  -
                  +Features | @mdf.js

                  Specifies the results to be returned from a query features Command

                  +

                  Enumeration Members

                  Enumeration Members

                  Pairs: "pairs"

                  List of supported Actions and applicable Targets

                  +
                  Profiles: "profiles"

                  List of profiles supported by this Actuator

                  +
                  RateLimit: "rate_limit"

                  Maximum number of Commands per minute supported by design or policy

                  +
                  Versions: "versions"

                  List of command and control Language versions supported by this Actuator

                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.L4Protocol.html b/docs/enums/_mdf.js_openc2-core.Control.L4Protocol.html similarity index 61% rename from docs/enums/_mdf_js_openc2_core.Control.L4Protocol.html rename to docs/enums/_mdf.js_openc2-core.Control.L4Protocol.html index 6b78045e..465d5c5a 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.L4Protocol.html +++ b/docs/enums/_mdf.js_openc2-core.Control.L4Protocol.html @@ -1,10 +1,10 @@ -L4Protocol | @mdf.js

                  Layer 4 protocol

                  -

                  Enumeration Members

                  Enumeration Members

                  ICMP: "icmp"

                  Internet Control Message Protocol - [RFC0792]

                  -
                  SCTP: "sctp"

                  Transmission Control Protocol - [RFC0793]

                  -
                  TCP: "tcp"

                  User Datagram Protocol - [RFC0768]

                  -
                  UDP: "udp"

                  Stream Control Transmission Protocol - [RFC4960]

                  -
                  +L4Protocol | @mdf.js

                  Layer 4 protocol

                  +

                  Enumeration Members

                  Enumeration Members

                  ICMP: "icmp"

                  Internet Control Message Protocol - [RFC0792]

                  +
                  SCTP: "sctp"

                  Transmission Control Protocol - [RFC0793]

                  +
                  TCP: "tcp"

                  User Datagram Protocol - [RFC0768]

                  +
                  UDP: "udp"

                  Stream Control Transmission Protocol - [RFC4960]

                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.MessageType.html b/docs/enums/_mdf.js_openc2-core.Control.MessageType.html similarity index 52% rename from docs/enums/_mdf_js_openc2_core.Control.MessageType.html rename to docs/enums/_mdf.js_openc2-core.Control.MessageType.html index 97077a68..53c4188d 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.MessageType.html +++ b/docs/enums/_mdf.js_openc2-core.Control.MessageType.html @@ -1,5 +1,5 @@ -MessageType | @mdf.js

                  Enumeration Members

                  Enumeration Members

                  Command: "command"

                  The Message content is an Command

                  -
                  Response: "response"

                  The Message content is an Response

                  -
                  +MessageType | @mdf.js

                  Enumeration Members

                  Enumeration Members

                  Command: "command"

                  The Message content is an Command

                  +
                  Response: "response"

                  The Message content is an Response

                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.ResponseType.html b/docs/enums/_mdf.js_openc2-core.Control.ResponseType.html similarity index 61% rename from docs/enums/_mdf_js_openc2_core.Control.ResponseType.html rename to docs/enums/_mdf.js_openc2-core.Control.ResponseType.html index 29556d44..87c01bf5 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.ResponseType.html +++ b/docs/enums/_mdf.js_openc2-core.Control.ResponseType.html @@ -1,10 +1,10 @@ -ResponseType | @mdf.js

                  Expected response type

                  -

                  Enumeration Members

                  Enumeration Members

                  ACK: "ack"

                  Respond when Command received

                  -
                  Complete: "complete"

                  Respond when all aspects of Command completed

                  -
                  None: "none"

                  No response

                  -
                  Status: "status"

                  Respond with progress toward Command completion

                  -
                  +ResponseType | @mdf.js

                  Expected response type

                  +

                  Enumeration Members

                  Enumeration Members

                  ACK: "ack"

                  Respond when Command received

                  +
                  Complete: "complete"

                  Respond when all aspects of Command completed

                  +
                  None: "none"

                  No response

                  +
                  Status: "status"

                  Respond with progress toward Command completion

                  +
                  diff --git a/docs/enums/_mdf_js_openc2_core.Control.StatusCode.html b/docs/enums/_mdf.js_openc2-core.Control.StatusCode.html similarity index 59% rename from docs/enums/_mdf_js_openc2_core.Control.StatusCode.html rename to docs/enums/_mdf.js_openc2-core.Control.StatusCode.html index febc38c9..a117117c 100644 --- a/docs/enums/_mdf_js_openc2_core.Control.StatusCode.html +++ b/docs/enums/_mdf.js_openc2-core.Control.StatusCode.html @@ -1,25 +1,25 @@ -StatusCode | @mdf.js

                  Status code enumeration

                  -

                  Enumeration Members

                  BadRequest: 400

                  The Consumer cannot process the Command due to something that is perceived to be a Producer +StatusCode | @mdf.js

                  Status code enumeration

                  +

                  Enumeration Members

                  BadRequest: 400

                  The Consumer cannot process the Command due to something that is perceived to be a Producer error (e.g., malformed Command syntax)

                  -
                  Forbidden: 403

                  The Consumer understood the Command but refuses to authorize it

                  -
                  InternalError: 500

                  The Consumer encountered an unexpected condition that prevented it from performing the +

                  Forbidden: 403

                  The Consumer understood the Command but refuses to authorize it

                  +
                  InternalError: 500

                  The Consumer encountered an unexpected condition that prevented it from performing the Command

                  -
                  NotFound: 404

                  The Consumer has not found anything matching the Command

                  -
                  NotImplemented: 501

                  The Consumer does not support the functionality required to perform the Command

                  -
                  OK: 200

                  The Command has succeeded

                  -
                  Processing: 102

                  An interim Response used to inform the Producer that the Consumer has accepted the Command but +

                  NotFound: 404

                  The Consumer has not found anything matching the Command

                  +
                  NotImplemented: 501

                  The Consumer does not support the functionality required to perform the Command

                  +
                  OK: 200

                  The Command has succeeded

                  +
                  Processing: 102

                  An interim Response used to inform the Producer that the Consumer has accepted the Command but has not yet completed it

                  -
                  ServiceUnavailable: 503

                  The Consumer is currently unable to perform the Command due to a temporary overloading or +

                  ServiceUnavailable: 503

                  The Consumer is currently unable to perform the Command due to a temporary overloading or maintenance of the Consumer

                  -
                  Unauthorized: 401

                  The Command Message lacks valid authentication credentials for the target resource or +

                  Unauthorized: 401

                  The Command Message lacks valid authentication credentials for the target resource or authorization has been refused for the submitted credentials

                  -
                  +
                  diff --git a/docs/enums/_mdf_js_tasks.LimiterState.html b/docs/enums/_mdf.js_tasks.LimiterState.html similarity index 63% rename from docs/enums/_mdf_js_tasks.LimiterState.html rename to docs/enums/_mdf.js_tasks.LimiterState.html index 8f5da7d0..0328517b 100644 --- a/docs/enums/_mdf_js_tasks.LimiterState.html +++ b/docs/enums/_mdf.js_tasks.LimiterState.html @@ -1,14 +1,14 @@ -LimiterState | @mdf.js

                  Enumeration LimiterState

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +LimiterState | @mdf.js

                  Enumeration LimiterState

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                  -

                  Enumeration Members

                  Enumeration Members

                  EMPTY: "empty"

                  The queue of task are empty but some tasks are running

                  -
                  IDLE: "idle"

                  The queue of task are empty and no tasks are running

                  -
                  RUNNING: "running"

                  The limiter is running, some tasks are running and some tasks are waiting

                  -
                  STARTING: "starting"

                  The limiter is starting, no tasks are running

                  -
                  STOPPED: "stopped"

                  The limiter is stopped, no tasks are running and no new tasks will be started

                  -
                  +

                  Enumeration Members

                  Enumeration Members

                  EMPTY: "empty"

                  The queue of task are empty but some tasks are running

                  +
                  IDLE: "idle"

                  The queue of task are empty and no tasks are running

                  +
                  RUNNING: "running"

                  The limiter is running, some tasks are running and some tasks are waiting

                  +
                  STARTING: "starting"

                  The limiter is starting, no tasks are running

                  +
                  STOPPED: "stopped"

                  The limiter is stopped, no tasks are running and no new tasks will be started

                  +
                  diff --git a/docs/enums/_mdf_js_tasks.RETRY_STRATEGY.html b/docs/enums/_mdf.js_tasks.RETRY_STRATEGY.html similarity index 53% rename from docs/enums/_mdf_js_tasks.RETRY_STRATEGY.html rename to docs/enums/_mdf.js_tasks.RETRY_STRATEGY.html index 42da823a..8052cf7a 100644 --- a/docs/enums/_mdf_js_tasks.RETRY_STRATEGY.html +++ b/docs/enums/_mdf.js_tasks.RETRY_STRATEGY.html @@ -1,12 +1,12 @@ -RETRY_STRATEGY | @mdf.js

                  Enumeration RETRY_STRATEGY

                  Represents the strategy to retry a task

                  -

                  Enumeration Members

                  FAIL_AFTER_EXECUTED: "failAfterExecuted"

                  The task will allow only one execution, if it fails, it will fail in every retry

                  -
                  FAIL_AFTER_SUCCESS: "failAfterSuccess"

                  The task will allow to be executed again if it fails, but it will rejects if there are more +RETRY_STRATEGY | @mdf.js

                  Enumeration RETRY_STRATEGY

                  Represents the strategy to retry a task

                  +

                  Enumeration Members

                  FAIL_AFTER_EXECUTED: "failAfterExecuted"

                  The task will allow only one execution, if it fails, it will fail in every retry

                  +
                  FAIL_AFTER_SUCCESS: "failAfterSuccess"

                  The task will allow to be executed again if it fails, but it will rejects if there are more retries before the success

                  -
                  NOT_EXEC_AFTER_SUCCESS: "notExecAfterSuccess"

                  The task will resolve the result of first successful execution, if it fails, it will allow to +

                  NOT_EXEC_AFTER_SUCCESS: "notExecAfterSuccess"

                  The task will resolve the result of first successful execution, if it fails, it will allow to be executed again

                  -
                  RETRY: "retry"

                  The task will allow to be executed again if it fails, updating the metadata in each retry

                  -
                  +
                  RETRY: "retry"

                  The task will allow to be executed again if it fails, updating the metadata in each retry

                  +
                  diff --git a/docs/enums/_mdf_js_tasks.STRATEGY.html b/docs/enums/_mdf.js_tasks.STRATEGY.html similarity index 64% rename from docs/enums/_mdf_js_tasks.STRATEGY.html rename to docs/enums/_mdf.js_tasks.STRATEGY.html index 29179934..c7c2ad59 100644 --- a/docs/enums/_mdf_js_tasks.STRATEGY.html +++ b/docs/enums/_mdf.js_tasks.STRATEGY.html @@ -1,22 +1,22 @@ -STRATEGY | @mdf.js

                  Enumeration STRATEGY

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +STRATEGY | @mdf.js

                  Enumeration STRATEGY

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                  -

                  Enumeration Members

                  Enumeration Members

                  BLOCK: "block"

                  When adding a new job to a limiter, if the queue length reaches highWater, the limiter falls +

                  Enumeration Members

                  Enumeration Members

                  BLOCK: "block"

                  When adding a new job to a limiter, if the queue length reaches highWater, the limiter falls into "blocked mode". All queued jobs are dropped and no new jobs will be accepted until the limiter unblocks. It will unblock after penalty milliseconds have passed without receiving a new job. penalty is equal to 15 * minTime (or 5000 if minTime is 0) by default. This strategy is ideal when bruteforce attacks are to be expected. This strategy totally ignores priority levels.

                  -
                  LEAK: "leak"

                  When adding a new job to a limiter, if the queue length reaches highWater, drop the oldest job +

                  LEAK: "leak"

                  When adding a new job to a limiter, if the queue length reaches highWater, drop the oldest job with the lowest priority. This is useful when jobs that have been waiting for too long are not important anymore. If all the queued jobs are more important (based on their priority value) than the one being added, it will not be added.

                  -
                  OVERFLOW: "overflow"

                  When adding a new job to a limiter, if the queue length reaches highWater, do not add the new +

                  OVERFLOW: "overflow"

                  When adding a new job to a limiter, if the queue length reaches highWater, do not add the new job. This strategy totally ignores priority levels.

                  -
                  OVERFLOW_PRIORITY: "overflow-priority"

                  Same as LEAK, except it will only drop jobs that are less important than the one being added. +

                  OVERFLOW_PRIORITY: "overflow-priority"

                  Same as LEAK, except it will only drop jobs that are less important than the one being added. If all the queued jobs are as or more important than the new one, it will not be added.

                  -
                  +
                  diff --git a/docs/enums/_mdf_js_tasks.TASK_STATE.html b/docs/enums/_mdf.js_tasks.TASK_STATE.html similarity index 63% rename from docs/enums/_mdf_js_tasks.TASK_STATE.html rename to docs/enums/_mdf.js_tasks.TASK_STATE.html index 1724cde1..1d2479f7 100644 --- a/docs/enums/_mdf_js_tasks.TASK_STATE.html +++ b/docs/enums/_mdf.js_tasks.TASK_STATE.html @@ -1,12 +1,12 @@ -TASK_STATE | @mdf.js

                  Enumeration TASK_STATE

                  Represent the state of a task

                  -

                  Enumeration Members

                  Enumeration Members

                  CANCELLED: "cancelled"

                  The task has been cancelled

                  -
                  COMPLETED: "completed"

                  The task has finished successfully

                  -
                  FAILED: "failed"

                  The task has finished with an error

                  -
                  PENDING: "pending"

                  The task has been created, but not yet started

                  -
                  RUNNING: "running"

                  The task is being processed in this moment

                  -
                  +TASK_STATE | @mdf.js

                  Enumeration TASK_STATE

                  Represent the state of a task

                  +

                  Enumeration Members

                  Enumeration Members

                  CANCELLED: "cancelled"

                  The task has been cancelled

                  +
                  COMPLETED: "completed"

                  The task has finished successfully

                  +
                  FAILED: "failed"

                  The task has finished with an error

                  +
                  PENDING: "pending"

                  The task has been created, but not yet started

                  +
                  RUNNING: "running"

                  The task is being processed in this moment

                  +
                  diff --git a/docs/functions/_mdf.js_core.Health.overallStatus.html b/docs/functions/_mdf.js_core.Health.overallStatus.html new file mode 100644 index 00000000..07e4b93d --- /dev/null +++ b/docs/functions/_mdf.js_core.Health.overallStatus.html @@ -0,0 +1,2 @@ +overallStatus | @mdf.js
                  diff --git a/docs/functions/_mdf.js_core.Layer.Provider.ProviderFactoryCreator.html b/docs/functions/_mdf.js_core.Layer.Provider.ProviderFactoryCreator.html new file mode 100644 index 00000000..d22b9867 --- /dev/null +++ b/docs/functions/_mdf.js_core.Layer.Provider.ProviderFactoryCreator.html @@ -0,0 +1,7 @@ +ProviderFactoryCreator | @mdf.js
                  diff --git a/docs/functions/_mdf.js_logger.SetContext.html b/docs/functions/_mdf.js_logger.SetContext.html new file mode 100644 index 00000000..cb61bc13 --- /dev/null +++ b/docs/functions/_mdf.js_logger.SetContext.html @@ -0,0 +1,5 @@ +SetContext | @mdf.js
                  • Create a wrapped version of the logger where the context and uuid are already set

                    +

                    Parameters

                    • logger: LoggerInstance

                      Logger instance to wrap

                      +
                    • context: string

                      context (class/function) where this logger is logging

                      +
                    • OptionalcomponentId: string

                      component identification

                      +

                    Returns WrapperLogger

                  diff --git a/docs/functions/_mdf.js_utils.coerce.html b/docs/functions/_mdf.js_utils.coerce.html new file mode 100644 index 00000000..edd0ffff --- /dev/null +++ b/docs/functions/_mdf.js_utils.coerce.html @@ -0,0 +1,20 @@ +coerce | @mdf.js
                  • Coerce an environment variable

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +

                    Returns Coercible

                  • Coerce an environment variable to a boolean value

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +
                    • alternative: boolean

                      default value

                      +

                    Returns boolean

                  • Coerce an environment variable to a numerical value

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +
                    • alternative: number

                      default value

                      +

                    Returns number

                  • Coerce an environment variable to an object

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +
                    • alternative: Record<string, any>

                      default value

                      +

                    Returns Record<string, any>

                  • Coerce an environment variable to an array

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +
                    • alternative: any[]

                      default value

                      +

                    Returns any[]

                  • Coerce an environment variable to a valid value

                    +

                    Type Parameters

                    Parameters

                    • env: undefined | string

                      Environment

                      +

                    Returns T | undefined

                  • Coerce an environment variable to a valid value

                    +

                    Parameters

                    • env: undefined | string

                      Environment

                      +
                    • alternative: any

                      default value

                      +

                    Returns any

                  diff --git a/docs/functions/_mdf.js_utils.deCycle.html b/docs/functions/_mdf.js_utils.deCycle.html new file mode 100644 index 00000000..739ded6d --- /dev/null +++ b/docs/functions/_mdf.js_utils.deCycle.html @@ -0,0 +1,5 @@ +deCycle | @mdf.js
                  • De-cycle an object to a JSON-safe representation

                    +

                    Parameters

                    • object: any

                      The object to be decycled

                      +
                    • Optionalreplacer: (value: any) => any

                      A function that transforms the object before decycling

                      +

                    Returns any

                    The decycled object

                    +
                  diff --git a/docs/functions/_mdf.js_utils.escapeRegExp.html b/docs/functions/_mdf.js_utils.escapeRegExp.html new file mode 100644 index 00000000..50029eb5 --- /dev/null +++ b/docs/functions/_mdf.js_utils.escapeRegExp.html @@ -0,0 +1,3 @@ +escapeRegExp | @mdf.js
                  • Escape a regular expression string.

                    +

                    Parameters

                    • regex: RegExp

                      Regular expression to escape.

                      +

                    Returns string

                  diff --git a/docs/functions/_mdf.js_utils.findNodeModule.html b/docs/functions/_mdf.js_utils.findNodeModule.html new file mode 100644 index 00000000..572b583d --- /dev/null +++ b/docs/functions/_mdf.js_utils.findNodeModule.html @@ -0,0 +1,5 @@ +findNodeModule | @mdf.js

                  Function findNodeModule

                  • Find the path to a node module in every parent directory (node_modules).

                    +

                    Parameters

                    • module: string

                      Module name

                      +
                    • dir: string = __dirname

                      Directory to start searching from

                      +

                    Returns string | undefined

                    Path to the module or undefined if not found

                    +
                  diff --git a/docs/functions/_mdf.js_utils.formatEnv.html b/docs/functions/_mdf.js_utils.formatEnv.html new file mode 100644 index 00000000..3ca1633e --- /dev/null +++ b/docs/functions/_mdf.js_utils.formatEnv.html @@ -0,0 +1,16 @@ +formatEnv | @mdf.js
                  • Read environment variables (process.env) and return an object with the values sanitized and the +keys formatted

                    +

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Returns T

                  • Read environment variables (process.env), filter them based in the indicated prefix, and return +an object with the values sanitized and the keys formatted

                    +

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      +

                    Returns T

                  • Read environment variables (process.env), filter them based in the indicated prefix, and return +an object with the values sanitized and the keys formatted based on the specified options

                    +

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      +
                    • options: Partial<ReadEnvOptions>

                      options to be used for key/value parsing and sanitize

                      +

                    Returns T

                  • Process a source, encoded as a environment variables file, filter them based in the indicated +prefix, and return an object with the values sanitized and the keys formatted based on the +specified options

                    +

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      +
                    • options: Partial<ReadEnvOptions>

                      options to be used for key/value parsing and sanitize

                      +
                    • source: Record<string, undefined | string>

                      source to be processed

                      +

                    Returns T

                  diff --git a/docs/functions/_mdf.js_utils.loadFile.html b/docs/functions/_mdf.js_utils.loadFile.html new file mode 100644 index 00000000..2e14496d --- /dev/null +++ b/docs/functions/_mdf.js_utils.loadFile.html @@ -0,0 +1,3 @@ +loadFile | @mdf.js
                  • Load the file from the path if this exist

                    +

                    Parameters

                    Returns Buffer | undefined

                  diff --git a/docs/functions/_mdf.js_utils.prettyMS.html b/docs/functions/_mdf.js_utils.prettyMS.html new file mode 100644 index 00000000..8811ed69 --- /dev/null +++ b/docs/functions/_mdf.js_utils.prettyMS.html @@ -0,0 +1,3 @@ +prettyMS | @mdf.js
                  • Convert milliseconds to a human readable string: 133700000015d 11h 23m 20s.

                    +

                    Parameters

                    • ms: number

                      Milliseconds to humanize.

                      +

                    Returns string

                  diff --git a/docs/functions/_mdf.js_utils.retroCycle.html b/docs/functions/_mdf.js_utils.retroCycle.html new file mode 100644 index 00000000..2fbaf86e --- /dev/null +++ b/docs/functions/_mdf.js_utils.retroCycle.html @@ -0,0 +1,4 @@ +retroCycle | @mdf.js
                  • Re-cycle an object to its original form

                    +

                    Parameters

                    • $: any

                      The object to be recycled

                      +

                    Returns any

                    The recycled object

                    +
                  diff --git a/docs/functions/_mdf.js_utils.retry.html b/docs/functions/_mdf.js_utils.retry.html new file mode 100644 index 00000000..dd88fc8c --- /dev/null +++ b/docs/functions/_mdf.js_utils.retry.html @@ -0,0 +1,5 @@ +retry | @mdf.js

                  Perform the retry functionality for a promise

                  +
                  diff --git a/docs/functions/_mdf.js_utils.retryBind.html b/docs/functions/_mdf.js_utils.retryBind.html new file mode 100644 index 00000000..3db80cdc --- /dev/null +++ b/docs/functions/_mdf.js_utils.retryBind.html @@ -0,0 +1,6 @@ +retryBind | @mdf.js

                  Perform the retry functionality for a promise

                  +
                  diff --git a/docs/functions/_mdf.js_utils.wrapOnRetry.html b/docs/functions/_mdf.js_utils.wrapOnRetry.html new file mode 100644 index 00000000..418c119e --- /dev/null +++ b/docs/functions/_mdf.js_utils.wrapOnRetry.html @@ -0,0 +1,8 @@ +wrapOnRetry | @mdf.js

                  Wraps a task with retry functionality.

                  +
                  • Type Parameters

                    • T

                    Parameters

                    Returns () => Promise<T>

                      +
                    • A function that, when called, executes the task with retry.
                    • +
                    +
                  diff --git a/docs/functions/_mdf_js_core.Health.overallStatus.html b/docs/functions/_mdf_js_core.Health.overallStatus.html deleted file mode 100644 index fa17e394..00000000 --- a/docs/functions/_mdf_js_core.Health.overallStatus.html +++ /dev/null @@ -1,2 +0,0 @@ -overallStatus | @mdf.js
                  diff --git a/docs/functions/_mdf_js_core.Layer.Provider.ProviderFactoryCreator.html b/docs/functions/_mdf_js_core.Layer.Provider.ProviderFactoryCreator.html deleted file mode 100644 index ddd5e6a3..00000000 --- a/docs/functions/_mdf_js_core.Layer.Provider.ProviderFactoryCreator.html +++ /dev/null @@ -1,7 +0,0 @@ -ProviderFactoryCreator | @mdf.js
                  diff --git a/docs/functions/_mdf_js_logger.SetContext.html b/docs/functions/_mdf_js_logger.SetContext.html deleted file mode 100644 index 01cc8150..00000000 --- a/docs/functions/_mdf_js_logger.SetContext.html +++ /dev/null @@ -1,5 +0,0 @@ -SetContext | @mdf.js
                  • Create a wrapped version of the logger where the context and uuid are already set

                    -

                    Parameters

                    • logger: LoggerInstance

                      Logger instance to wrap

                      -
                    • context: string

                      context (class/function) where this logger is logging

                      -
                    • OptionalcomponentId: string

                      component identification

                      -

                    Returns WrapperLogger

                  diff --git a/docs/functions/_mdf_js_utils.coerce.html b/docs/functions/_mdf_js_utils.coerce.html deleted file mode 100644 index f0662384..00000000 --- a/docs/functions/_mdf_js_utils.coerce.html +++ /dev/null @@ -1,20 +0,0 @@ -coerce | @mdf.js
                  • Coerce an environment variable

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -

                    Returns Coerceable

                  • Coerce an environment variable to a boolean value

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -
                    • alternative: boolean

                      default value

                      -

                    Returns boolean

                  • Coerce an environment variable to a numerical value

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -
                    • alternative: number

                      default value

                      -

                    Returns number

                  • Coerce an environment variable to an object

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -
                    • alternative: Record<string, any>

                      default value

                      -

                    Returns Record<string, any>

                  • Coerce an environment variable to an array

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -
                    • alternative: any[]

                      default value

                      -

                    Returns any[]

                  • Coerce an environment variable to a valid value

                    -

                    Type Parameters

                    • T extends Coerceable

                    Parameters

                    • env: undefined | string

                      Environment

                      -

                    Returns T | undefined

                  • Coerce an environment variable to a valid value

                    -

                    Parameters

                    • env: undefined | string

                      Environment

                      -
                    • alternative: any

                      default value

                      -

                    Returns any

                  diff --git a/docs/functions/_mdf_js_utils.deCycle.html b/docs/functions/_mdf_js_utils.deCycle.html deleted file mode 100644 index 377ab89e..00000000 --- a/docs/functions/_mdf_js_utils.deCycle.html +++ /dev/null @@ -1,5 +0,0 @@ -deCycle | @mdf.js
                  • De-cycle an object to a JSON-safe representation

                    -

                    Parameters

                    • object: any

                      The object to be decycled

                      -
                    • Optionalreplacer: ((value: any) => any)

                      A function that transforms the object before decycling

                      -
                        • (value): any
                        • Parameters

                          • value: any

                          Returns any

                    Returns any

                    The decycled object

                    -
                  diff --git a/docs/functions/_mdf_js_utils.escapeRegExp.html b/docs/functions/_mdf_js_utils.escapeRegExp.html deleted file mode 100644 index e6775a3e..00000000 --- a/docs/functions/_mdf_js_utils.escapeRegExp.html +++ /dev/null @@ -1,3 +0,0 @@ -escapeRegExp | @mdf.js
                  • Escape a regular expression string.

                    -

                    Parameters

                    • regex: RegExp

                      Regular expression to escape.

                      -

                    Returns string

                  diff --git a/docs/functions/_mdf_js_utils.findNodeModule.html b/docs/functions/_mdf_js_utils.findNodeModule.html deleted file mode 100644 index f944d69e..00000000 --- a/docs/functions/_mdf_js_utils.findNodeModule.html +++ /dev/null @@ -1,5 +0,0 @@ -findNodeModule | @mdf.js

                  Function findNodeModule

                  • Find the path to a node module in every parent directory (node_modules).

                    -

                    Parameters

                    • module: string

                      Module name

                      -
                    • dir: string = __dirname

                      Directory to start searching from

                      -

                    Returns string | undefined

                    Path to the module or undefined if not found

                    -
                  diff --git a/docs/functions/_mdf_js_utils.formatEnv.html b/docs/functions/_mdf_js_utils.formatEnv.html deleted file mode 100644 index f378f7d9..00000000 --- a/docs/functions/_mdf_js_utils.formatEnv.html +++ /dev/null @@ -1,16 +0,0 @@ -formatEnv | @mdf.js
                  • Read environment variables (process.env) and return an object with the values sanitized and the -keys formatted

                    -

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Returns T

                  • Read environment variables (process.env), filter them based in the indicated prefix, and return -an object with the values sanitized and the keys formatted

                    -

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      -

                    Returns T

                  • Read environment variables (process.env), filter them based in the indicated prefix, and return -an object with the values sanitized and the keys formatted based on the specified options

                    -

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      -
                    • options: Partial<ReadEnvOptions>

                      options to be used for key/value parsing and sanitize

                      -

                    Returns T

                  • Process a source, encoded as a environment variables file, filter them based in the indicated -prefix, and return an object with the values sanitized and the keys formatted based on the -specified options

                    -

                    Type Parameters

                    • T extends Record<string, any> = Record<string, any>

                    Parameters

                    • prefix: string

                      prefix to filter

                      -
                    • options: Partial<ReadEnvOptions>

                      options to be used for key/value parsing and sanitize

                      -
                    • source: Record<string, undefined | string>

                      source to be processed

                      -

                    Returns T

                  diff --git a/docs/functions/_mdf_js_utils.loadFile.html b/docs/functions/_mdf_js_utils.loadFile.html deleted file mode 100644 index 40addc72..00000000 --- a/docs/functions/_mdf_js_utils.loadFile.html +++ /dev/null @@ -1,3 +0,0 @@ -loadFile | @mdf.js
                  • Load the file from the path if this exist

                    -

                    Parameters

                    • Optionalpath: string

                      path to file

                      -
                    • Optionallogger: LoggerInstance

                    Returns Buffer | undefined

                  diff --git a/docs/functions/_mdf_js_utils.prettyMS.html b/docs/functions/_mdf_js_utils.prettyMS.html deleted file mode 100644 index 1f5f2f5b..00000000 --- a/docs/functions/_mdf_js_utils.prettyMS.html +++ /dev/null @@ -1,3 +0,0 @@ -prettyMS | @mdf.js
                  • Convert milliseconds to a human readable string: 133700000015d 11h 23m 20s.

                    -

                    Parameters

                    • ms: number

                      Milliseconds to humanize.

                      -

                    Returns string

                  diff --git a/docs/functions/_mdf_js_utils.retroCycle.html b/docs/functions/_mdf_js_utils.retroCycle.html deleted file mode 100644 index 01b5f6d9..00000000 --- a/docs/functions/_mdf_js_utils.retroCycle.html +++ /dev/null @@ -1,4 +0,0 @@ -retroCycle | @mdf.js
                  • Re-cycle an object to its original form

                    -

                    Parameters

                    • $: any

                      The object to be recycled

                      -

                    Returns any

                    The recycled object

                    -
                  diff --git a/docs/functions/_mdf_js_utils.retry.html b/docs/functions/_mdf_js_utils.retry.html deleted file mode 100644 index 7375ec0f..00000000 --- a/docs/functions/_mdf_js_utils.retry.html +++ /dev/null @@ -1,5 +0,0 @@ -retry | @mdf.js

                  Perform the retry functionality for a promise

                  -
                  • Type Parameters

                    • T

                    Parameters

                    Returns Promise<T>

                  diff --git a/docs/functions/_mdf_js_utils.retryBind.html b/docs/functions/_mdf_js_utils.retryBind.html deleted file mode 100644 index 50150e39..00000000 --- a/docs/functions/_mdf_js_utils.retryBind.html +++ /dev/null @@ -1,6 +0,0 @@ -retryBind | @mdf.js

                  Perform the retry functionality for a promise

                  -
                  • Type Parameters

                    • T
                    • U

                    Parameters

                    Returns Promise<T>

                  diff --git a/docs/functions/_mdf_js_utils.wrapOnRetry.html b/docs/functions/_mdf_js_utils.wrapOnRetry.html deleted file mode 100644 index 18427c53..00000000 --- a/docs/functions/_mdf_js_utils.wrapOnRetry.html +++ /dev/null @@ -1,8 +0,0 @@ -wrapOnRetry | @mdf.js

                  Wraps a task with retry functionality.

                  -
                  • Type Parameters

                    • T

                    Parameters

                    Returns (() => Promise<T>)

                      -
                    • A function that, when called, executes the task with retry.
                    • -
                    -
                      • (): Promise<T>
                      • Returns Promise<T>

                  diff --git a/docs/hierarchy.html b/docs/hierarchy.html index 94d94f83..682a54ca 100644 --- a/docs/hierarchy.html +++ b/docs/hierarchy.html @@ -1 +1 @@ -@mdf.js
                  +@mdf.js

                  @mdf.js

                  Hierarchy Summary

                  diff --git a/docs/index.html b/docs/index.html index 93067e93..4b1cc6a4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -@mdf.js

                  @mdf.js

                  @mdf.js

                  Node Version +@mdf.js

                  @mdf.js

                  @mdf.js

                  Node Version Typescript Version Known Vulnerabilities Build Status @@ -13,20 +13,20 @@

                  Mytra Development Framework - @mdf

                  Typescript framework for core application development

                  -
                    -
                  • @mdf.js + -

                    Mytra Development Framework is a set of tools for development TypeScript projects. It is based on the @mdf.js organization and is composed of several packages that can be used independently but that are designed to work together. The main idea of the framework is to integrate the most common tools and packages used in the development of TypeScript projects, usually wrapping them in a simpler interface and adding some extra functionality. For example the providers offered by this framework wrap the most common packages for managing connections to different services, such as rhea-promise for AMQP connections, @elastic/elasticsearch for Elastic, ... and offer an unified interface to create, manage and diagnose them. These interfaces are integrated with the observability component, so when an error occurs in one of the providers, the error is registered in the observability interface.

                    -

                    Each module can be installed separately, for example:

                    +

                    Mytra Development Framework is a set of tools for development TypeScript projects. It is based on the @mdf.js organization and is composed of several packages that can be used independently but that are designed to work together. The main idea of the framework is to integrate the most common tools and packages used in the development of TypeScript projects, usually wrapping them in a simpler interface and adding some extra functionality. For example the providers offered by this framework wrap the most common packages for managing connections to different services, such as rhea-promise for AMQP connections, @elastic/elasticsearch for Elastic, ... and offer an unified interface to create, manage and diagnose them. These interfaces are integrated with the observability component, so when an error occurs in one of the providers, the error is registered in the observability interface.

                    +

                    Each module can be installed separately, for example:

                    NPM:

                    npm install @mdf.js/crash
                     
                    @@ -35,7 +35,7 @@
                    Typescript framework for core applica
                    yarn add @mdf.js/crash
                     
                    -

                    The complete framework is composed of the following packages:

                    +

                    The complete framework is composed of the following packages:

                    -

                    Check the documentation of each package for more information.

                    -

                    Copyright 2024 Mytra Control S.L. All rights reserved.

                    +

                    Check the documentation of each package for more information.

                    +

                    Copyright 2024 Mytra Control S.L. All rights reserved.

                    Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                    -
                  +
                  diff --git a/docs/interfaces/_mdf_js_core.Health.Check.html b/docs/interfaces/_mdf.js_core.Health.Check.html similarity index 52% rename from docs/interfaces/_mdf_js_core.Health.Check.html rename to docs/interfaces/_mdf.js_core.Health.Check.html index 0da54fe0..fc9c431b 100644 --- a/docs/interfaces/_mdf_js_core.Health.Check.html +++ b/docs/interfaces/_mdf.js_core.Health.Check.html @@ -1,24 +1,24 @@ -Check | @mdf.js
                  interface Check<T> {
                      affectedEndpoints?: string[];
                      componentId: string;
                      componentType?: string;
                      links?: {
                          about?: string;
                          related?: string;
                          self?: string;
                      };
                      observedUnit?: string;
                      observedValue?: T;
                      output?: string | string[];
                      status: "pass" | "fail" | "warn";
                      time?: string;
                      [x: string]: any;
                  }

                  Type Parameters

                  • T = any

                  Indexable

                  • [x: string]: any

                    Any other desired property

                    -

                  Properties

                  affectedEndpoints?: string[]

                  A JSON array containing URI Templates as defined by [RFC6570]. This field SHOULD be omitted if +Check | @mdf.js

                  interface Check<T = any> {
                      affectedEndpoints?: string[];
                      componentId: string;
                      componentType?: string;
                      links?: { about?: string; related?: string; self?: string };
                      observedUnit?: string;
                      observedValue?: T;
                      output?: string | string[];
                      status: "pass" | "fail" | "warn";
                      time?: string;
                      [x: string]: any;
                  }

                  Type Parameters

                  • T = any

                  Indexable

                  • [x: string]: any

                    Any other desired property

                    +

                  Properties

                  affectedEndpoints?: string[]

                  A JSON array containing URI Templates as defined by [RFC6570]. This field SHOULD be omitted if the "status" field is present and has value equal to "pass". A typical API has many URI endpoints. Most of the time we are interested in the overall health of the API, without diving into details. That said, sometimes operational and resilience middleware needs to know more details about the health of the API (which is why "checks" property provides details). In such cases, we often need to indicate which particular endpoints are affected by a particular check's troubles vs. other endpoints that may be fine.

                  -
                  componentId: string

                  Unique identifier of an instance of a specific sub-component/dependency of a service. +

                  componentId: string

                  Unique identifier of an instance of a specific sub-component/dependency of a service. Multiple objects with the same componentID MAY appear in the details, if they are from different nodes.

                  -
                  componentType?: string

                  SHOULD be present if componentName is present. Type of the component. Could be one of:

                  +
                  componentType?: string

                  SHOULD be present if componentName is present. Type of the component. Could be one of:

                  • Pre-defined value from this spec. Pre-defined values include:
                      @@ -35,12 +35,12 @@ They are just a namespace, and the meaning of a namespace CAN be provided by any convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book). -
                  links?: {
                      about?: string;
                      related?: string;
                      self?: string;
                  }

                  An array of objects containing link relations and URIs [RFC3986] for external links that MAY +

                  links?: { about?: string; related?: string; self?: string }

                  An array of objects containing link relations and URIs [RFC3986] for external links that MAY contain more information about the health of the endpoint. Per web-linking standards [RFC5988] a link relationship SHOULD either be a common/registered one or be indicated as a URI, to avoid name clashes. If a “self” link is provided, it MAY be used by clients to check health via HTTP response code, as mentioned above.

                  -
                  observedUnit?: string

                  SHOULD be present if metricValue is present. Could be one of:

                  +
                  observedUnit?: string

                  SHOULD be present if metricValue is present. Could be one of:

                  • A common and standard term from a well-known source such as schema.org, IANA or microformats.
                  • @@ -49,10 +49,10 @@ They are just a namespace, and the meaning of a namespace CAN be provided by any convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book).
                  -
                  observedValue?: T

                  Could be any valid JSON value, such as: string, number, object, array or literal

                  -
                  output?: string | string[]

                  Raw error output, in case of “fail” or “warn” states. This field SHOULD be omitted for +

                  observedValue?: T

                  Could be any valid JSON value, such as: string, number, object, array or literal

                  +
                  output?: string | string[]

                  Raw error output, in case of “fail” or “warn” states. This field SHOULD be omitted for “pass” state.

                  -
                  status: "pass" | "fail" | "warn"
                  time?: string

                  The date-time, in ISO8601 format, at which the reading of the metricValue was recorded. This +

                  status: "pass" | "fail" | "warn"
                  time?: string

                  The date-time, in ISO8601 format, at which the reading of the metricValue was recorded. This assumes that the value can be cached and the reading typically doesn’t happen in real time, for performance and scalability purposes.

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.DefaultOptions.html b/docs/interfaces/_mdf.js_core.Jobs.DefaultOptions.html new file mode 100644 index 00000000..b44cb8a5 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.DefaultOptions.html @@ -0,0 +1,6 @@ +DefaultOptions | @mdf.js

                  Interface DefaultOptions<CustomHeaders>

                  interface DefaultOptions<CustomHeaders extends Record<string, any> = AnyHeaders> {
                      headers?: CustomHeaders;
                      numberOfHandlers?: number;
                  }

                  Type Parameters

                  • CustomHeaders extends Record<string, any> = AnyHeaders

                  Properties

                  headers?: CustomHeaders

                  Job meta information, used to pass specific information for jobs handlers

                  +
                  numberOfHandlers?: number

                  Indicates the number of handlers that must be successfully processed to consider the job as +successfully processed

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.JobObject.html b/docs/interfaces/_mdf.js_core.Jobs.JobObject.html new file mode 100644 index 00000000..d7f54a4f --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.JobObject.html @@ -0,0 +1,16 @@ +JobObject | @mdf.js

                  Interface JobObject<Type, Data, CustomHeaders, CustomOptions>

                  Job object

                  +
                  interface JobObject<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      data: Data;
                      jobUserId: string;
                      jobUserUUID: string;
                      options?: Options<CustomHeaders, CustomOptions>;
                      status: Jobs.Status;
                      type: Type;
                      uuid: string;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Implemented by

                  Properties

                  data: Data

                  Job payload

                  +
                  jobUserId: string

                  User job request identifier, defined by the user

                  +
                  jobUserUUID: string

                  Unique user job request identification, generated by UUID V5 standard and based on jobUserId

                  +

                  Job meta information, used to pass specific information for job processors

                  +
                  status: Jobs.Status

                  Job status

                  +
                  type: Type

                  Job type identification, used to identify specific job handlers to be applied

                  +
                  uuid: string

                  Unique job processing identification

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.JobRequest.html b/docs/interfaces/_mdf.js_core.Jobs.JobRequest.html new file mode 100644 index 00000000..2e58dc66 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.JobRequest.html @@ -0,0 +1,9 @@ +JobRequest | @mdf.js

                  Interface JobRequest<Type, Data, CustomHeaders, CustomOptions>

                  interface JobRequest<
                      Type extends string = string,
                      Data = unknown,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      data: Data;
                      jobUserId: string;
                      options?: Options<CustomHeaders, CustomOptions>;
                      type?: Type;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = unknown
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Properties

                  data: Data

                  Job payload

                  +
                  jobUserId: string

                  User job request identifier, defined by the user

                  +

                  Job meta information, used to pass specific information for job processors

                  +
                  type?: Type

                  Job type identification, used to identify specific job handlers to be applied

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.NoMoreHeaders.html b/docs/interfaces/_mdf.js_core.Jobs.NoMoreHeaders.html new file mode 100644 index 00000000..bf702eb1 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.NoMoreHeaders.html @@ -0,0 +1,2 @@ +NoMoreHeaders | @mdf.js

                  No more extra headers information

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.NoMoreOptions.html b/docs/interfaces/_mdf.js_core.Jobs.NoMoreOptions.html new file mode 100644 index 00000000..da6c4087 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.NoMoreOptions.html @@ -0,0 +1,2 @@ +NoMoreOptions | @mdf.js

                  No more extra options information

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.Result.html b/docs/interfaces/_mdf.js_core.Jobs.Result.html new file mode 100644 index 00000000..1b20dc48 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.Result.html @@ -0,0 +1,22 @@ +Result | @mdf.js

                  Interface Result<Type>

                  Job result interface

                  +
                  interface Result<Type extends string = string> {
                      createdAt: string;
                      errors?: MultiObject;
                      hasErrors: boolean;
                      jobUserId: string;
                      jobUserUUID: string;
                      quantity: number;
                      resolvedAt: string;
                      status: Jobs.Status;
                      type: Type;
                      uuid: string;
                  }

                  Type Parameters

                  • Type extends string = string

                  Properties

                  createdAt: string

                  Timestamp, in ISO format, of the job creation date

                  +
                  errors?: MultiObject

                  Array of errors

                  +
                  hasErrors: boolean

                  Flag that indicate that the publication process has some errors

                  +
                  jobUserId: string

                  User job request identifier, defined by the user

                  +
                  jobUserUUID: string

                  Unique user job request identification, based on jobUserId

                  +
                  quantity: number

                  Number of entities processed with success in this job

                  +
                  resolvedAt: string

                  Timestamp, in ISO format, of the job resolve date

                  +
                  status: Jobs.Status

                  Job status

                  +
                  type: Type

                  Job type

                  +
                  uuid: string

                  Unique job processing identification

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Jobs.Strategy.html b/docs/interfaces/_mdf.js_core.Jobs.Strategy.html new file mode 100644 index 00000000..bd77e080 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Jobs.Strategy.html @@ -0,0 +1,7 @@ +Strategy | @mdf.js

                  Interface Strategy<Type, Data, CustomHeaders, CustomOptions>

                  Base class for strategies

                  +
                  interface Strategy<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      do: (
                          process: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                      ) => JobObject<Type, Data, CustomHeaders, CustomOptions>;
                      name: string;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Properties

                  do +name +

                  Properties

                  Perform the filter of the data based in concrete criteria

                  +

                  Type declaration

                  name: string

                  Strategy name

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Layer.App.Component.html b/docs/interfaces/_mdf.js_core.Layer.App.Component.html new file mode 100644 index 00000000..a7fe6d99 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.App.Component.html @@ -0,0 +1,12 @@ +Component | @mdf.js

                  A component is any part of the system that has a own identity and can be monitored for error +handling. The only requirement is to emit an error event when something goes wrong, to have a +name and unique component identifier.

                  +
                  interface Component {
                      componentId: string;
                      name: string;
                      on(event: "error", listener: (error: Multi | Error | Crash) => void): this;
                  }

                  Hierarchy (View Summary)

                  Implemented by

                  Properties

                  Methods

                  on +

                  Properties

                  componentId: string

                  Component identifier

                  +
                  name: string

                  Component name

                  +

                  Methods

                  • Add a listener for the error event, emitted when the component detects an error.

                    +

                    Parameters

                    • event: "error"

                      error event

                      +
                    • listener: (error: Multi | Error | Crash) => void

                      Error event listener

                      +

                    Returns this

                  diff --git a/docs/interfaces/_mdf.js_core.Layer.App.Health.html b/docs/interfaces/_mdf.js_core.Layer.App.Health.html new file mode 100644 index 00000000..92c979dc --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.App.Health.html @@ -0,0 +1,101 @@ +Health | @mdf.js

                  Health service interface

                  +
                  interface Health {
                      checks?: Checks;
                      description?: string;
                      instanceId: string;
                      links?: { about?: string; related?: string; self?: string };
                      name: string;
                      namespace?: `x-${string}`;
                      notes?: string[];
                      output?: string;
                      release: string;
                      serviceGroupId?: string;
                      serviceId?: string;
                      status: "pass" | "fail" | "warn";
                      tags?: string[];
                      version: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  checks?: Checks

                  The “checks” object MAY have a number of unique keys, one for each logical sub-components. +Since each sub-component may be backed by several nodes with varying health statuses, the key +points to an array of objects. In case of a single-node sub-component (or if presence of nodes +is not relevant), a single-element array should be used as the value, for consistency. +The key identifying an element in the object should be a unique string within the details +section. It MAY have two parts: {componentName}:{metricName}, in which case the meaning of +the parts SHOULD be as follows:

                  +
                    +
                  • componentName: Human-readable name for the component. MUST not contain a colon, in the name, +since colon is used as a separator
                  • +
                  • metricName: Name of the metrics that the status is reported for. MUST not contain a colon, +in the name, since colon is used as a separator and can be one of: +
                      +
                    • Pre-defined value from this spec. Pre-defined values include: +
                        +
                      • utilization
                      • +
                      • responseTime
                      • +
                      • connections
                      • +
                      • uptime
                      • +
                      +
                    • +
                    • A common and standard term from a well-known source such as schema.org, IANA or +microformats.
                    • +
                    • A URI that indicates extra semantics and processing rules that MAY be provided by a +resource at the other end of the URI. URIs do not have to be dereferenceable, however. +They are just a namespace, and the meaning of a namespace CAN be provided by any +convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book).
                    • +
                    +
                  • +
                  +
                  description?: string

                  Service description

                  +
                  `My own service description`
                  +
                  + +
                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  +
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  +
                  + +
                  links?: { about?: string; related?: string; self?: string }

                  Service related links

                  +

                  Type declaration

                  • Optionalabout?: string

                    About link for the service

                    +
                    `https://www.mytra.es/about`
                    +
                    + +
                  • Optionalrelated?: string

                    Related link for the service

                    +
                    `https://www.mytra.es`
                    +
                    + +
                  • Optionalself?: string

                    Link to the own service or health endpoint

                    +
                    `http://localhost:3000/v1/health`
                    +
                    + +
                  name: string

                  Service name

                  +
                  `myOwnService`
                  +
                  + +
                  namespace?: `x-${string}`

                  Service namespace, used to identify declare which namespace the service belongs to. +It must start with x- as it is a custom namespace and will be used for custom headers, +openc2 commands, etc.

                  +
                  `x-mytra`
                  +
                  + +
                  notes?: string[]

                  Array of notes relevant to current state of health

                  +
                  output?: string

                  Raw error output, in case of “fail” or “warn” states. This field SHOULD be omitted for +“pass” state.

                  +
                  release: string

                  Service release. Its recommended to use semantic versioning.

                  +
                  `1.0.0`
                  +
                  + +
                  serviceGroupId?: string

                  Service group unique identification

                  +
                  `firehose`, `driver`
                  +
                  + +
                  serviceId?: string

                  Service unique identification

                  +
                  `uplink-firehose`, `mqtt-driver`
                  +
                  + +
                  status: "pass" | "fail" | "warn"

                  Indicates whether the service status is acceptable or not

                  +
                  tags?: string[]

                  List of string values that can be used to add service-level labels.

                  +
                  `["primary", "test"]`
                  +
                  + +
                  version: string

                  Service version

                  +
                  `1`
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_core.Layer.App.Metadata.html b/docs/interfaces/_mdf.js_core.Layer.App.Metadata.html new file mode 100644 index 00000000..80a58954 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.App.Metadata.html @@ -0,0 +1,63 @@ +Metadata | @mdf.js

                  Application definition

                  +
                  interface Metadata {
                      description?: string;
                      instanceId: string;
                      links?: { about?: string; related?: string; self?: string };
                      name: string;
                      namespace?: `x-${string}`;
                      release: string;
                      serviceGroupId?: string;
                      serviceId?: string;
                      tags?: string[];
                      version: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  description?: string

                  Service description

                  +
                  `My own service description`
                  +
                  + +
                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  +
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  +
                  + +
                  links?: { about?: string; related?: string; self?: string }

                  Service related links

                  +

                  Type declaration

                  • Optionalabout?: string

                    About link for the service

                    +
                    `https://www.mytra.es/about`
                    +
                    + +
                  • Optionalrelated?: string

                    Related link for the service

                    +
                    `https://www.mytra.es`
                    +
                    + +
                  • Optionalself?: string

                    Link to the own service or health endpoint

                    +
                    `http://localhost:3000/v1/health`
                    +
                    + +
                  name: string

                  Service name

                  +
                  `myOwnService`
                  +
                  + +
                  namespace?: `x-${string}`

                  Service namespace, used to identify declare which namespace the service belongs to. +It must start with x- as it is a custom namespace and will be used for custom headers, +openc2 commands, etc.

                  +
                  `x-mytra`
                  +
                  + +
                  release: string

                  Service release. Its recommended to use semantic versioning.

                  +
                  `1.0.0`
                  +
                  + +
                  serviceGroupId?: string

                  Service group unique identification

                  +
                  `firehose`, `driver`
                  +
                  + +
                  serviceId?: string

                  Service unique identification

                  +
                  `uplink-firehose`, `mqtt-driver`
                  +
                  + +
                  tags?: string[]

                  List of string values that can be used to add service-level labels.

                  +
                  `["primary", "test"]`
                  +
                  + +
                  version: string

                  Service version

                  +
                  `1`
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_core.Layer.App.Resource.html b/docs/interfaces/_mdf.js_core.Layer.App.Resource.html new file mode 100644 index 00000000..05b816d8 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.App.Resource.html @@ -0,0 +1,28 @@ +Resource | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface Resource {
                      checks: Checks;
                      close: () => Promise<void>;
                      componentId: string;
                      name: string;
                      start: () => Promise<void>;
                      status: "pass" | "fail" | "warn";
                      stop: () => Promise<void>;
                      on(event: "error", listener: (error: Multi | Error | Crash) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                  }

                  Hierarchy (View Summary)

                  Implemented by

                  Properties

                  Methods

                  on +

                  Properties

                  checks: Checks

                  Checks performed over this component to achieve the resulted status

                  +
                  close: () => Promise<void>

                  Resource close function

                  +
                  componentId: string

                  Component identifier

                  +
                  name: string

                  Component name

                  +
                  start: () => Promise<void>

                  Resource start function

                  +
                  status: "pass" | "fail" | "warn"

                  Resource status

                  +
                  stop: () => Promise<void>

                  Resource stop function

                  +

                  Methods

                  • Add a listener for the error event, emitted when the component detects an error.

                    +

                    Parameters

                    • event: "error"

                      error event

                      +
                    • listener: (error: Multi | Error | Crash) => void

                      Error event listener

                      +

                    Returns this

                  • Add a listener for the status event, emitted when the component status changes.

                    +

                    Parameters

                    • event: "status"

                      status event

                      +
                    • listener: (status: "pass" | "fail" | "warn") => void

                      Status event listener

                      +

                    Returns this

                  diff --git a/docs/interfaces/_mdf.js_core.Layer.App.Service.html b/docs/interfaces/_mdf.js_core.Layer.App.Service.html new file mode 100644 index 00000000..fe79fc90 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.App.Service.html @@ -0,0 +1,37 @@ +Service | @mdf.js

                  A service is a special kind of resource that besides Resource properties, it could offer:

                  +
                    +
                  • Its own REST API endpoints, using an express router, to expose details about service, this +endpoints will be exposed under the observability paths.
                  • +
                  • A links property to define the endpoints that the service expose, this information will be +exposed in the observability paths.
                  • +
                  • A metrics property to expose the metrics of the service, this registry will be merged with the +global metrics registry.
                  • +
                  +
                  interface Service {
                      checks: Checks;
                      close: () => Promise<void>;
                      componentId: string;
                      links?: Links;
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      name: string;
                      router?: Router;
                      start: () => Promise<void>;
                      status: "pass" | "fail" | "warn";
                      stop: () => Promise<void>;
                      on(event: "error", listener: (error: Multi | Error | Crash) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                  }

                  Hierarchy (View Summary)

                  Implemented by

                  Properties

                  checks: Checks

                  Checks performed over this component to achieve the resulted status

                  +
                  close: () => Promise<void>

                  Resource close function

                  +
                  componentId: string

                  Component identifier

                  +
                  links?: Links

                  Service base path

                  +
                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Get the metrics registry

                  +
                  name: string

                  Component name

                  +
                  router?: Router

                  Express router

                  +
                  start: () => Promise<void>

                  Resource start function

                  +
                  status: "pass" | "fail" | "warn"

                  Resource status

                  +
                  stop: () => Promise<void>

                  Resource stop function

                  +

                  Methods

                  • Add a listener for the error event, emitted when the component detects an error.

                    +

                    Parameters

                    • event: "error"

                      error event

                      +
                    • listener: (error: Multi | Error | Crash) => void

                      Error event listener

                      +

                    Returns this

                  • Add a listener for the status event, emitted when the component status changes.

                    +

                    Parameters

                    • event: "status"

                      status event

                      +
                    • listener: (status: "pass" | "fail" | "warn") => void

                      Status event listener

                      +

                    Returns this

                  diff --git a/docs/interfaces/_mdf.js_core.Layer.Provider.Factory.html b/docs/interfaces/_mdf.js_core.Layer.Provider.Factory.html new file mode 100644 index 00000000..9e309ab6 --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.Provider.Factory.html @@ -0,0 +1,5 @@ +Factory | @mdf.js

                  Interface Factory<PortClient, PortConfig, T>

                  Provider factory interface

                  +
                  interface Factory<
                      PortClient,
                      PortConfig,
                      T extends Layer.Provider.Port<PortClient, PortConfig>,
                  > {
                      create(
                          options?: FactoryOptions<PortConfig>,
                      ): Manager<PortClient, PortConfig, T>;
                  }

                  Type Parameters

                  Methods

                  Methods

                  diff --git a/docs/interfaces/_mdf_js_core.Layer.Provider.FactoryOptions.html b/docs/interfaces/_mdf.js_core.Layer.Provider.FactoryOptions.html similarity index 51% rename from docs/interfaces/_mdf_js_core.Layer.Provider.FactoryOptions.html rename to docs/interfaces/_mdf.js_core.Layer.Provider.FactoryOptions.html index 79cef9d8..002dd135 100644 --- a/docs/interfaces/_mdf_js_core.Layer.Provider.FactoryOptions.html +++ b/docs/interfaces/_mdf.js_core.Layer.Provider.FactoryOptions.html @@ -1,14 +1,14 @@ -FactoryOptions | @mdf.js

                  Interface FactoryOptions<PortConfig>

                  Factory configuration options

                  -
                  interface FactoryOptions<PortConfig> {
                      config?: Partial<PortConfig>;
                      logger?: LoggerInstance;
                      name?: string;
                      useEnvironment?: string | boolean;
                  }

                  Type Parameters

                  • PortConfig

                  Properties

                  config?: Partial<PortConfig>

                  Specific port configuration options

                  -

                  Port and provider logger, to be used internally

                  +FactoryOptions | @mdf.js

                  Interface FactoryOptions<PortConfig>

                  Factory configuration options

                  +
                  interface FactoryOptions<PortConfig> {
                      config?: Partial<PortConfig>;
                      logger?: LoggerInstance;
                      name?: string;
                      useEnvironment?: string | boolean;
                  }

                  Type Parameters

                  • PortConfig

                  Properties

                  config?: Partial<PortConfig>

                  Specific port configuration options

                  +

                  Port and provider logger, to be used internally

                  name?: string

                  Provider name, used for human-readable logs and identification

                  -
                  useEnvironment?: string | boolean

                  Flag indicating that the environment configuration variables should be used, merged with +

                  useEnvironment?: string | boolean

                  Flag indicating that the environment configuration variables should be used, merged with the default values and the configuration passed as argument to the provider.

                  If a string is passed this will be used as prefix for the environment configuration variables, represented in SCREAMING_SNAKE_CASE, that will parsed to camelCase and merged with the rest of the configuration.

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Layer.Provider.PortConfigValidationStruct.html b/docs/interfaces/_mdf.js_core.Layer.Provider.PortConfigValidationStruct.html new file mode 100644 index 00000000..688a915b --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.Provider.PortConfigValidationStruct.html @@ -0,0 +1,9 @@ +PortConfigValidationStruct | @mdf.js

                  Interface PortConfigValidationStruct<PortConfig>

                  Port configuration validation structure

                  +
                  interface PortConfigValidationStruct<PortConfig> {
                      defaultConfig: PortConfig;
                      envBasedConfig: PortConfig;
                      schema: Schema<PortConfig>;
                  }

                  Type Parameters

                  • PortConfig

                    Port configuration object, could be an extended version of the client config

                    +

                  Properties

                  defaultConfig: PortConfig

                  Default configuration options

                  +
                  envBasedConfig: PortConfig

                  Environment based configuration options

                  +
                  schema: Schema<PortConfig>

                  Schema for configuration validation

                  +
                  diff --git a/docs/interfaces/_mdf.js_core.Layer.Provider.ProviderOptions.html b/docs/interfaces/_mdf.js_core.Layer.Provider.ProviderOptions.html new file mode 100644 index 00000000..44df7bbe --- /dev/null +++ b/docs/interfaces/_mdf.js_core.Layer.Provider.ProviderOptions.html @@ -0,0 +1,25 @@ +ProviderOptions | @mdf.js

                  Interface ProviderOptions<PortConfig>

                  Provider configuration options

                  +
                  interface ProviderOptions<PortConfig> {
                      logger?: LoggerInstance;
                      name: string;
                      type: string;
                      useEnvironment?: string | boolean;
                      validation: PortConfigValidationStruct<PortConfig>;
                  }

                  Type Parameters

                  • PortConfig

                    Port configuration object, could be an extended version of the client config

                    +

                  Properties

                  Port and provider logger, to be used internally

                  +
                  name: string

                  Provider name, used for human-readable logs and identification

                  +
                  type: string

                  Provider type, kind of component form the points of view of the health check standard +https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06

                  +
                  useEnvironment?: string | boolean

                  String is used as prefix for the environment configuration variables, represented in +SCREAMING_SNAKE_CASE, that will parsed to camelCase and merged with the rest of the +configuration.

                  +
                  // Environment variables
                  process.env.PORT_NAME_TYPE = 'http';
                  process.env.PORT_NAME_HOST = 'localhost';
                  process.env.PORT_NAME_PORT = '8080';
                  process.env.PORT_NAME_OTHER_CONFIG__MY_CONFIG = 'true'; +
                  + +
                  // Provider configuration
                  {
                  name: 'port-name',
                  type: 'http',
                  validation: {...},
                  envPrefix: 'PORT_NAME_',
                  } +
                  + +
                  // Resulting configuration
                  {
                  type: 'http',
                  host: 'localhost',
                  port: 8080,
                  otherConfig: {
                  myConfig: true,
                  },
                  } +
                  + +

                  Port validation options

                  +
                  diff --git a/docs/interfaces/_mdf.js_core._internal_.State.html b/docs/interfaces/_mdf.js_core._internal_.State.html new file mode 100644 index 00000000..50ba80bf --- /dev/null +++ b/docs/interfaces/_mdf.js_core._internal_.State.html @@ -0,0 +1,11 @@ +State | @mdf.js

                  Provider state interface

                  +
                  interface State {
                      state: "error" | "running" | "stopped";
                      fail(error: Error | Crash): Promise<void>;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Properties

                  Methods

                  Properties

                  state: "error" | "running" | "stopped"

                  Actual provider state

                  +

                  Methods

                  • Go to error state: waiting for new state o auto-fix de the problems

                    +

                    Parameters

                    • error: Error | Crash

                      incoming error from provider

                      +

                    Returns Promise<void>

                  • Initialize the process: internal jobs, external dependencies connections ...

                    +

                    Returns Promise<void>

                  • Stop the process: internal jobs, external dependencies connections ...

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf_js_crash.APIError.html b/docs/interfaces/_mdf.js_crash.APIError.html similarity index 60% rename from docs/interfaces/_mdf_js_crash.APIError.html rename to docs/interfaces/_mdf.js_crash.APIError.html index 48341db6..e7ff9258 100644 --- a/docs/interfaces/_mdf_js_crash.APIError.html +++ b/docs/interfaces/_mdf.js_crash.APIError.html @@ -1,15 +1,15 @@ -APIError | @mdf.js

                  Standardized response interface in REST API services for errors

                  -
                  interface APIError {
                      code: string;
                      detail?: string;
                      links?: Links;
                      meta?: {
                          [x: string]: any;
                      };
                      source?: APISource;
                      status: number;
                      title: string;
                      uuid: string;
                  }

                  Properties

                  code -detail? -links? -meta? -source? -status -title -uuid +APIError | @mdf.js

                  Standardized response interface in REST API services for errors

                  +
                  interface APIError {
                      code: string;
                      detail?: string;
                      links?: Links;
                      meta?: { [x: string]: any };
                      source?: APISource;
                      status: number;
                      title: string;
                      uuid: string;
                  }

                  Properties

                  code: string

                  REST API specific error code

                  detail?: string

                  Human-readable explanation specific to this occurrence of the problem

                  -
                  links?: Links

                  Links that leads to further details about this particular occurrence of the problem. +

                  links?: Links

                  Links that leads to further details about this particular occurrence of the problem. A link MUST be represented as either:

                  • self: a string containing the link’s URL
                  • @@ -20,9 +20,9 @@
                  -
                  meta?: {
                      [x: string]: any;
                  }

                  A meta object containing non-standard meta-information about the error

                  -
                  source?: APISource

                  An object containing references to the source of the error

                  +
                  meta?: { [x: string]: any }

                  A meta object containing non-standard meta-information about the error

                  +
                  source?: APISource

                  An object containing references to the source of the error

                  status: number

                  HTTP Status code

                  title: string

                  Human-readable summary of problem that SHOULD NOT change from occurrence to occurrence

                  uuid: string

                  UUID V4, unique identifier for this particular occurrence of the problem

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.APISource.html b/docs/interfaces/_mdf.js_crash.APISource.html new file mode 100644 index 00000000..b426eacd --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.APISource.html @@ -0,0 +1,6 @@ +APISource | @mdf.js

                  Object with the key information of the requested resource in the REST API context

                  +
                  interface APISource {
                      parameter: { [x: string]: any };
                      pointer: string;
                  }

                  Properties

                  Properties

                  parameter: { [x: string]: any }

                  A string indicating which URI query parameter caused the error

                  +
                  pointer: string

                  Pointer to the associated resource in the request [e.g."data/job/title"]

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.BoomOptions.html b/docs/interfaces/_mdf.js_crash.BoomOptions.html new file mode 100644 index 00000000..c4cf6c06 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.BoomOptions.html @@ -0,0 +1,13 @@ +BoomOptions | @mdf.js

                  Interface BoomOptions

                  Boom error configuration options

                  +
                  interface BoomOptions {
                      cause?: Cause;
                      info?: { date?: Date; subject?: string; [x: string]: any };
                      links?: Links;
                      name?: string;
                      source?: APISource;
                      [x: string]: unknown;
                  }

                  Hierarchy (View Summary)

                  Indexable

                  • [x: string]: unknown

                    Other key information from extends error

                    +

                  Properties

                  Properties

                  cause?: Cause
                  info?: { date?: Date; subject?: string; [x: string]: any }

                  Extra information error

                  +

                  Type declaration

                  • [x: string]: any

                    Any other relevant information

                    +
                  • Optionaldate?: Date

                    Date of the error

                    +
                  • Optionalsubject?: string

                    Subject to which the error relates

                    +
                  links?: Links
                  name?: string

                  Name of the error, used as a category

                  +
                  source?: APISource
                  diff --git a/docs/interfaces/_mdf_js_crash.Context.html b/docs/interfaces/_mdf.js_crash.Context.html similarity index 50% rename from docs/interfaces/_mdf_js_crash.Context.html rename to docs/interfaces/_mdf.js_crash.Context.html index 7b788bef..3806f26b 100644 --- a/docs/interfaces/_mdf_js_crash.Context.html +++ b/docs/interfaces/_mdf.js_crash.Context.html @@ -1,5 +1,5 @@ -Context | @mdf.js

                  Context interface from Joi library

                  -
                  interface Context {
                      key?: string;
                      label?: string;
                      value?: any;
                      [key: string]: any;
                  }

                  Indexable

                  • [key: string]: any

                  Properties

                  Properties

                  key?: string
                  label?: string
                  value?: any
                  +Context | @mdf.js

                  Context interface from Joi library

                  +
                  interface Context {
                      key?: string;
                      label?: string;
                      value?: any;
                      [key: string]: any;
                  }

                  Indexable

                  • [key: string]: any

                  Properties

                  Properties

                  key?: string
                  label?: string
                  value?: any
                  diff --git a/docs/interfaces/_mdf.js_crash.CrashObject.html b/docs/interfaces/_mdf.js_crash.CrashObject.html new file mode 100644 index 00000000..5474d9e3 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.CrashObject.html @@ -0,0 +1,16 @@ +CrashObject | @mdf.js

                  Interface CrashObject

                  Crash error object output

                  +
                  interface CrashObject {
                      info?: Record<string, unknown>;
                      message: string;
                      name: string;
                      subject: string;
                      timestamp: string;
                      trace: string[];
                      uuid: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  info?: Record<string, unknown>

                  Extra error information

                  +
                  message: string

                  Human friendly error message

                  +
                  name: string

                  Name of the error

                  +
                  subject: string

                  Error subject

                  +
                  timestamp: string

                  Timestamp of the error

                  +
                  trace: string[]

                  Stack of error messages arranged according to the hierarchy of errors and causes

                  +
                  uuid: string

                  Identification of the process, request or transaction where the error appears

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.CrashOptions.html b/docs/interfaces/_mdf.js_crash.CrashOptions.html new file mode 100644 index 00000000..7403bfc6 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.CrashOptions.html @@ -0,0 +1,12 @@ +CrashOptions | @mdf.js

                  Interface CrashOptions

                  Crash error configuration options

                  +
                  interface CrashOptions {
                      cause?: Cause;
                      info?: { date?: Date; subject?: string; [x: string]: any };
                      name?: string;
                      [x: string]: unknown;
                  }

                  Hierarchy (View Summary)

                  Indexable

                  • [x: string]: unknown

                    Other key information from extends error

                    +

                  Properties

                  Properties

                  cause?: Cause

                  Error that caused the creation of this instance

                  +
                  info?: { date?: Date; subject?: string; [x: string]: any }

                  Extra information error

                  +

                  Type declaration

                  • [x: string]: any

                    Any other relevant information

                    +
                  • Optionaldate?: Date

                    Date of the error

                    +
                  • Optionalsubject?: string

                    Subject to which the error relates

                    +
                  name?: string

                  Name of the error, used as a category

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.MultiObject.html b/docs/interfaces/_mdf.js_crash.MultiObject.html new file mode 100644 index 00000000..ef84dd91 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.MultiObject.html @@ -0,0 +1,16 @@ +MultiObject | @mdf.js

                  Interface MultiObject

                  Multi error object output

                  +
                  interface MultiObject {
                      info?: Record<string, unknown>;
                      message: string;
                      name: string;
                      subject: string;
                      timestamp: string;
                      trace: string[];
                      uuid: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  info?: Record<string, unknown>

                  Extra error information

                  +
                  message: string

                  Human friendly error message

                  +
                  name: string

                  Name of the error

                  +
                  subject: string

                  Error subject

                  +
                  timestamp: string

                  Timestamp of the error

                  +
                  trace: string[]

                  Stack of error messages arranged according to the hierarchy of errors and causes

                  +
                  uuid: string

                  Identification of the process, request or transaction where the error appears

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.MultiOptions.html b/docs/interfaces/_mdf.js_crash.MultiOptions.html new file mode 100644 index 00000000..15d9c1ac --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.MultiOptions.html @@ -0,0 +1,12 @@ +MultiOptions | @mdf.js

                  Interface MultiOptions

                  Multi error configuration options

                  +
                  interface MultiOptions {
                      causes?: Error | Crash | (Error | Crash)[];
                      info?: { date?: Date; subject?: string; [x: string]: any };
                      name?: string;
                      [x: string]: unknown;
                  }

                  Hierarchy (View Summary)

                  Indexable

                  • [x: string]: unknown

                    Other key information from extends error

                    +

                  Properties

                  Properties

                  causes?: Error | Crash | (Error | Crash)[]

                  Errors that caused the creation of this instance

                  +
                  info?: { date?: Date; subject?: string; [x: string]: any }

                  Extra information error

                  +

                  Type declaration

                  • [x: string]: any

                    Any other relevant information

                    +
                  • Optionaldate?: Date

                    Date of the error

                    +
                  • Optionalsubject?: string

                    Subject to which the error relates

                    +
                  name?: string

                  Name of the error, used as a category

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash.ValidationError.html b/docs/interfaces/_mdf.js_crash.ValidationError.html new file mode 100644 index 00000000..2f4377c1 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.ValidationError.html @@ -0,0 +1,12 @@ +ValidationError | @mdf.js

                  Interface ValidationError

                  ValidationError interface from Joi library

                  +
                  interface ValidationError {
                      _original: any;
                      details: ValidationErrorItem[];
                      isJoi: boolean;
                      name: "ValidationError";
                      annotate(stripColors?: boolean): string;
                  }

                  Hierarchy

                  • Error
                    • ValidationError

                  Properties

                  Methods

                  Properties

                  _original: any

                  Array of errors

                  +
                  isJoi: boolean
                  name: "ValidationError"

                  Methods

                  • Function that returns a string with an annotated version of the object pointing at the places +where errors occurred. +NOTE: This method does not exist in browser builds of Joi

                    +

                    Parameters

                    • OptionalstripColors: boolean

                      if truthy, will strip the colors out of the output.

                      +

                    Returns string

                  diff --git a/docs/interfaces/_mdf.js_crash.ValidationErrorItem.html b/docs/interfaces/_mdf.js_crash.ValidationErrorItem.html new file mode 100644 index 00000000..08159176 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash.ValidationErrorItem.html @@ -0,0 +1,6 @@ +ValidationErrorItem | @mdf.js

                  Interface ValidationErrorItem

                  ValidationErrorItem interface from Joi library

                  +
                  interface ValidationErrorItem {
                      context?: Context;
                      message: string;
                      path: (string | number)[];
                      type: string;
                  }

                  Properties

                  Properties

                  context?: Context
                  message: string
                  path: (string | number)[]
                  type: string
                  diff --git a/docs/interfaces/_mdf.js_crash._internal_.BaseObject.html b/docs/interfaces/_mdf.js_crash._internal_.BaseObject.html new file mode 100644 index 00000000..f38d1e23 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash._internal_.BaseObject.html @@ -0,0 +1,16 @@ +BaseObject | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface BaseObject {
                      info?: Record<string, unknown>;
                      message: string;
                      name: string;
                      subject: string;
                      timestamp: string;
                      uuid: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  info?: Record<string, unknown>

                  Extra error information

                  +
                  message: string

                  Human friendly error message

                  +
                  name: string

                  Name of the error

                  +
                  subject: string

                  Error subject

                  +
                  timestamp: string

                  Timestamp of the error

                  +
                  uuid: string

                  Identification of the process, request or transaction where the error appears

                  +
                  diff --git a/docs/interfaces/_mdf.js_crash._internal_.BaseOptions.html b/docs/interfaces/_mdf.js_crash._internal_.BaseOptions.html new file mode 100644 index 00000000..a0d60db0 --- /dev/null +++ b/docs/interfaces/_mdf.js_crash._internal_.BaseOptions.html @@ -0,0 +1,9 @@ +BaseOptions | @mdf.js
                  interface BaseOptions {
                      info?: { date?: Date; subject?: string; [x: string]: any };
                      name?: string;
                      [x: string]: unknown;
                  }

                  Hierarchy (View Summary)

                  Indexable

                  • [x: string]: unknown

                    Other key information from extends error

                    +

                  Properties

                  Properties

                  info?: { date?: Date; subject?: string; [x: string]: any }

                  Extra information error

                  +

                  Type declaration

                  • [x: string]: any

                    Any other relevant information

                    +
                  • Optionaldate?: Date

                    Date of the error

                    +
                  • Optionalsubject?: string

                    Subject to which the error relates

                    +
                  name?: string

                  Name of the error, used as a category

                  +
                  diff --git a/docs/interfaces/_mdf.js_doorkeeper.DoorkeeperOptions.html b/docs/interfaces/_mdf.js_doorkeeper.DoorkeeperOptions.html new file mode 100644 index 00000000..3e2ecddf --- /dev/null +++ b/docs/interfaces/_mdf.js_doorkeeper.DoorkeeperOptions.html @@ -0,0 +1,5 @@ +DoorkeeperOptions | @mdf.js

                  This is the AJV Options object, but allErrors property is always true by default

                  +

                  See AJV Options for more information

                  +
                  interface DoorkeeperOptions {
                      dynamicDefaults?: Record<string, DynamicDefaultFunc>;
                  }

                  Hierarchy

                  • Omit<Options, "allErrors">
                    • DoorkeeperOptions

                  Properties

                  Properties

                  dynamicDefaults?: Record<string, DynamicDefaultFunc>

                  Dynamic defaults to be used in the schemas

                  +
                  diff --git a/docs/interfaces/_mdf_js_faker.DefaultObject.html b/docs/interfaces/_mdf.js_faker.DefaultObject.html similarity index 50% rename from docs/interfaces/_mdf_js_faker.DefaultObject.html rename to docs/interfaces/_mdf.js_faker.DefaultObject.html index 181c1ebd..f723fcf5 100644 --- a/docs/interfaces/_mdf_js_faker.DefaultObject.html +++ b/docs/interfaces/_mdf.js_faker.DefaultObject.html @@ -1,2 +1,2 @@ -DefaultObject | @mdf.js

                  Interface DefaultObject

                  Interface for default object

                  -

                  Indexable

                  • [key: string]: any
                  +DefaultObject | @mdf.js

                  Interface DefaultObject

                  Interface for default object

                  +

                  Indexable

                  • [key: string]: any
                  diff --git a/docs/interfaces/_mdf.js_faker.DefaultOptions.html b/docs/interfaces/_mdf.js_faker.DefaultOptions.html new file mode 100644 index 00000000..5c9d8cf6 --- /dev/null +++ b/docs/interfaces/_mdf.js_faker.DefaultOptions.html @@ -0,0 +1,3 @@ +DefaultOptions | @mdf.js

                  Interface DefaultOptions

                  Interface for default options

                  +
                  interface DefaultOptions {
                      likelihood?: number;
                      [key: string]: any;
                  }

                  Indexable

                  • [key: string]: any

                  Properties

                  Properties

                  likelihood?: number
                  diff --git a/docs/interfaces/_mdf.js_faker._internal_.Entry.html b/docs/interfaces/_mdf.js_faker._internal_.Entry.html new file mode 100644 index 00000000..18373d80 --- /dev/null +++ b/docs/interfaces/_mdf.js_faker._internal_.Entry.html @@ -0,0 +1,4 @@ +Entry | @mdf.js

                  Interface for attribute generation

                  +
                  interface Entry<T> {
                      builder: Builder<T, keyof T>;
                      dependencies?: Dependencies<T, keyof T>;
                  }

                  Type Parameters

                  • T

                  Properties

                  Properties

                  builder: Builder<T, keyof T>
                  dependencies?: Dependencies<T, keyof T>
                  diff --git a/docs/interfaces/_mdf.js_file-flinger.FileFlingerOptions.html b/docs/interfaces/_mdf.js_file-flinger.FileFlingerOptions.html new file mode 100644 index 00000000..4502a3e5 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger.FileFlingerOptions.html @@ -0,0 +1,49 @@ +FileFlingerOptions | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface FileFlingerOptions {
                      archiveFolder?: string;
                      cwd?: string;
                      deadLetterFolder?: string;
                      defaultValues?: Record<string, string>;
                      errorStrategy?: ErrorStrategy;
                      failedOperationDelay?: number;
                      filePattern?: string;
                      keyPattern?: string;
                      logger?: LoggerInstance;
                      maxErrors?: number;
                      postProcessingStrategy?: PostProcessingStrategy;
                      pushers: Pusher[];
                      retryOptions?: RetryOptions;
                      watchPath?: string | string[];
                  }

                  Hierarchy (View Summary)

                  Properties

                  archiveFolder?: string

                  Archive folder for processed files

                  +
                  cwd?: string

                  The base path to use

                  +
                  deadLetterFolder?: string

                  Dead-letter folder for files with keying errors

                  +
                  defaultValues?: Record<string, string>

                  The default values for the keys

                  +
                  errorStrategy?: ErrorStrategy

                  Determine the error strategy for files with errors

                  +
                  failedOperationDelay?: number

                  Delay between retries failed file processing operations

                  +
                  filePattern?: string

                  File pattern to match the files to process

                  +
                  keyPattern?: string

                  Key pattern used to generate the key for the file in the pusher. +The key pattern can contain placeholders that will be replaced by the actual values. +The placeholders are enclosed in curly braces and the following are supported:

                  +
                    +
                  • {_filename}: The name of the file
                  • +
                  • {_extension}: The extension of the file
                  • +
                  • {_timestamp}: The timestamp when the file was created or modified in milliseconds
                  • +
                  • {_date}: The date when the file was created or modified in the format YYYY-MM-DD
                  • +
                  • {_time}: The time when the file was created or modified in the format HH-mm-ss
                  • +
                  • {_datetime}: The date and time when the file was created or modified in the format +YYYY-MM-DD_HH-mm-ss
                  • +
                  • {_year}: The year when the file was created or modified
                  • +
                  • {_month}: The month when the file was created or modified
                  • +
                  • {_day}: The day when the file was created or modified
                  • +
                  • {_hour}: The hour when the file was created or modified
                  • +
                  • {_minute}: The minute when the file was created or modified
                  • +
                  • {_second}: The second when the file was created or modified
                  • +
                  +

                  Logger instance for deep debugging tasks

                  +
                  maxErrors?: number

                  Max number of errors to store

                  +
                  postProcessingStrategy?: PostProcessingStrategy

                  Determine the post-processing strategy for files without errors

                  +
                  pushers: Pusher[]

                  Pushers to send the files to

                  +
                  retryOptions?: RetryOptions

                  Retry options for file operations

                  +
                  watchPath?: string | string[]

                  The path to watch

                  +
                  diff --git a/docs/interfaces/_mdf.js_file-flinger.Pusher.html b/docs/interfaces/_mdf.js_file-flinger.Pusher.html new file mode 100644 index 00000000..2f9eb399 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger.Pusher.html @@ -0,0 +1,6 @@ +Pusher | @mdf.js

                  Pusher interface

                  +
                  interface Pusher {
                      push(filePath: string, key: string): Promise<void>;
                  }

                  Hierarchy (View Summary)

                  Methods

                  Methods

                  • Push the file to the storage

                    +

                    Parameters

                    • filePath: string

                      The file path to push

                      +
                    • key: string

                      The key to use

                      +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_file-flinger._internal_.EngineOptions.html b/docs/interfaces/_mdf.js_file-flinger._internal_.EngineOptions.html new file mode 100644 index 00000000..5da29a58 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger._internal_.EngineOptions.html @@ -0,0 +1,20 @@ +EngineOptions | @mdf.js

                  Engine options

                  +
                  interface EngineOptions {
                      archiveFolder?: string;
                      componentId: string;
                      deadLetterFolder?: string;
                      errorStrategy?: ErrorStrategy;
                      failedOperationDelay?: number;
                      name: string;
                      postProcessingStrategy?: PostProcessingStrategy;
                      pushers: Pusher[];
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  archiveFolder?: string

                  Archive folder for processed files

                  +
                  componentId: string

                  The component identifier

                  +
                  deadLetterFolder?: string

                  Dead-letter folder for files with keying errors

                  +
                  errorStrategy?: ErrorStrategy

                  Determine the error strategy for files with errors

                  +
                  failedOperationDelay?: number

                  Delay between retries failed file processing operations

                  +
                  name: string

                  The name of the watcher

                  +
                  postProcessingStrategy?: PostProcessingStrategy

                  Determine the post-processing strategy for files without errors

                  +
                  pushers: Pusher[]

                  Pushers to send the files to

                  +
                  retryOptions?: RetryOptions

                  Retry options for file operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_file-flinger._internal_.ErroredFile.html b/docs/interfaces/_mdf.js_file-flinger._internal_.ErroredFile.html new file mode 100644 index 00000000..7d2b56f7 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger._internal_.ErroredFile.html @@ -0,0 +1,8 @@ +ErroredFile | @mdf.js

                  The ErroredFile interface

                  +
                  interface ErroredFile {
                      errorTrace: string[];
                      path: string;
                      strategy: ErrorStrategy;
                  }

                  Properties

                  Properties

                  errorTrace: string[]

                  The error message

                  +
                  path: string

                  The file path

                  +
                  strategy: ErrorStrategy

                  The error strategy to applied

                  +
                  diff --git a/docs/interfaces/_mdf.js_file-flinger._internal_.FileTasksOptions.html b/docs/interfaces/_mdf.js_file-flinger._internal_.FileTasksOptions.html new file mode 100644 index 00000000..64b277c9 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger._internal_.FileTasksOptions.html @@ -0,0 +1,14 @@ +FileTasksOptions | @mdf.js

                  File tasks options

                  +
                  interface FileTasksOptions {
                      archiveFolder?: string;
                      deadLetterFolder?: string;
                      errorStrategy?: ErrorStrategy;
                      postProcessingStrategy?: PostProcessingStrategy;
                      pushers: Pusher[];
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  archiveFolder?: string

                  Archive folder for processed files

                  +
                  deadLetterFolder?: string

                  Dead-letter folder for files with keying errors

                  +
                  errorStrategy?: ErrorStrategy

                  Determine the error strategy for files with errors

                  +
                  postProcessingStrategy?: PostProcessingStrategy

                  Determine the post-processing strategy for files without errors

                  +
                  pushers: Pusher[]

                  Pushers to send the files to

                  +
                  retryOptions?: RetryOptions

                  Retry options for file operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_file-flinger._internal_.KeygenOptions.html b/docs/interfaces/_mdf.js_file-flinger._internal_.KeygenOptions.html new file mode 100644 index 00000000..2acc0e19 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger._internal_.KeygenOptions.html @@ -0,0 +1,27 @@ +KeygenOptions | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface KeygenOptions {
                      defaultValues?: Record<string, string>;
                      filePattern?: string;
                      keyPattern?: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  defaultValues?: Record<string, string>

                  The default values for the keys

                  +
                  filePattern?: string

                  File pattern to match the files to process

                  +
                  keyPattern?: string

                  Key pattern used to generate the key for the file in the pusher. +The key pattern can contain placeholders that will be replaced by the actual values. +The placeholders are enclosed in curly braces and the following are supported:

                  +
                    +
                  • {_filename}: The name of the file
                  • +
                  • {_extension}: The extension of the file
                  • +
                  • {_timestamp}: The timestamp when the file was created or modified in milliseconds
                  • +
                  • {_date}: The date when the file was created or modified in the format YYYY-MM-DD
                  • +
                  • {_time}: The time when the file was created or modified in the format HH-mm-ss
                  • +
                  • {_datetime}: The date and time when the file was created or modified in the format +YYYY-MM-DD_HH-mm-ss
                  • +
                  • {_year}: The year when the file was created or modified
                  • +
                  • {_month}: The month when the file was created or modified
                  • +
                  • {_day}: The day when the file was created or modified
                  • +
                  • {_hour}: The hour when the file was created or modified
                  • +
                  • {_minute}: The minute when the file was created or modified
                  • +
                  • {_second}: The second when the file was created or modified
                  • +
                  +
                  diff --git a/docs/interfaces/_mdf.js_file-flinger._internal_.WatcherOptions.html b/docs/interfaces/_mdf.js_file-flinger._internal_.WatcherOptions.html new file mode 100644 index 00000000..de0d65f5 --- /dev/null +++ b/docs/interfaces/_mdf.js_file-flinger._internal_.WatcherOptions.html @@ -0,0 +1,12 @@ +WatcherOptions | @mdf.js

                  Watcher options

                  +
                  interface WatcherOptions {
                      componentId?: string;
                      cwd?: string;
                      maxErrors?: number;
                      name?: string;
                      watchPath?: string | string[];
                  }

                  Properties

                  componentId?: string

                  The component identifier

                  +
                  cwd?: string

                  The base path to use

                  +
                  maxErrors?: number

                  Max number of errors to store

                  +
                  name?: string

                  The name of the watcher

                  +
                  watchPath?: string | string[]

                  The path to watch

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose.FirehoseOptions.html b/docs/interfaces/_mdf.js_firehose.FirehoseOptions.html new file mode 100644 index 00000000..3e1cb54a --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.FirehoseOptions.html @@ -0,0 +1,19 @@ +FirehoseOptions | @mdf.js

                  Interface FirehoseOptions<Type, Data, CustomHeaders, CustomOptions>

                  interface FirehoseOptions<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      atLeastOne?: boolean;
                      bufferSize?: number;
                      logger?: LoggerInstance;
                      maxInactivityTime?: number;
                      postConsumeOptions?: PostConsumeOptions;
                      retryOptions?: RetryOptions;
                      sinks: Plugs.Sink.Any<Type, Data, CustomHeaders, CustomOptions>[];
                      sources: Plugs.Source.Any<Type, Data, CustomHeaders, CustomOptions>[];
                      strategies?: {
                          [type: string]: Jobs.Strategy<Type, Data, CustomHeaders, CustomOptions>[];
                      };
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Properties

                  atLeastOne?: boolean

                  Define the number of sinks that must confirm a job, default options is all of them

                  +
                  bufferSize?: number

                  Buffer sizes

                  +

                  Logger instance for deep debugging tasks

                  +
                  maxInactivityTime?: number

                  Maximum time of inactivity before the firehose notify that is hold

                  +
                  postConsumeOptions?: PostConsumeOptions

                  Post consume operation options

                  +
                  retryOptions?: RetryOptions

                  Retry options for sink/source operations

                  +

                  Firehose sinks

                  +

                  Firehose sources

                  +
                  strategies?: {
                      [type: string]: Jobs.Strategy<Type, Data, CustomHeaders, CustomOptions>[];
                  }

                  Firehose transformation strategies per job type

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Jet.html b/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Jet.html new file mode 100644 index 00000000..8c21ace1 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Jet.html @@ -0,0 +1,19 @@ +Jet | @mdf.js

                  Interface Jet<Type, Data, CustomHeaders, CustomOptions>

                  Jet Sink interface definition +A Jet is a Sink that allows to manage multiple Jobs at a time using the multi method or one Job +using the single method

                  +
                  interface Jet<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      multi: (
                          jobs: JobObject<Type, Data, CustomHeaders, CustomOptions>[],
                      ) => Promise<void>;
                      single: (
                          job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                      ) => Promise<void>;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  multi: (
                      jobs: JobObject<Type, Data, CustomHeaders, CustomOptions>[],
                  ) => Promise<void>

                  Perform the processing of several Jobs

                  +

                  Type declaration

                  single: (
                      job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                  ) => Promise<void>

                  Perform the processing of a single Job

                  +

                  Type declaration

                  Methods

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Tap.html b/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Tap.html new file mode 100644 index 00000000..dd60f4a9 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.Plugs.Sink.Tap.html @@ -0,0 +1,15 @@ +Tap | @mdf.js

                  Interface Tap<Type, Data, CustomHeaders, CustomOptions>

                  Tap Sink interface definition +A Tap is a Sink that only allows to manage one Job at a time using the single method

                  +
                  interface Tap<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      single: (
                          job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                      ) => Promise<void>;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  single: (
                      job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                  ) => Promise<void>

                  Perform the processing of a single Job

                  +

                  Type declaration

                  Methods

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose.Plugs.Source.CreditsFlow.html b/docs/interfaces/_mdf.js_firehose.Plugs.Source.CreditsFlow.html new file mode 100644 index 00000000..678f5a46 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.Plugs.Source.CreditsFlow.html @@ -0,0 +1,24 @@ +CreditsFlow | @mdf.js

                  Interface CreditsFlow<Type, Data, CustomHeaders, CustomOptions>

                  CreditsFlow Source interface definition +A CreditsFlow is a Source that allows to manage the flow of Jobs using a credit system to control +the rate of Jobs that can be processed

                  +
                  interface CreditsFlow<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      postConsume: (jobId: string) => Promise<undefined | string>;
                      addCredits(credits: number): Promise<number>;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      on(
                          event: "data",
                          listener: (
                              job: JobRequest<Type, Data, CustomHeaders, CustomOptions>,
                          ) => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  postConsume: (jobId: string) => Promise<undefined | string>

                  Perform the task to clean the job registers after the job has been resolved

                  +

                  Type declaration

                    • (jobId: string): Promise<undefined | string>
                    • Parameters

                      • jobId: string

                        Job entry identification

                        +

                      Returns Promise<undefined | string>

                        +
                      • the job entry identification that has been correctly removed or undefined if the job +was not found
                      • +
                      +

                  Methods

                  • Add new credits to the source

                    +

                    Parameters

                    • credits: number

                      Credits to be added to the source

                      +

                    Returns Promise<number>

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Emitted when there is a new job to be managed

                    +

                    Parameters

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose.Plugs.Source.Flow.html b/docs/interfaces/_mdf.js_firehose.Plugs.Source.Flow.html new file mode 100644 index 00000000..6af1f158 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.Plugs.Source.Flow.html @@ -0,0 +1,25 @@ +Flow | @mdf.js

                  Interface Flow<Type, Data, CustomHeaders, CustomOptions>

                  Flow Source interface definition +A Flow is a Source that allows to manage the flow of Jobs using init/pause" methods to control +the rate of Jobs that can be processed

                  +
                  interface Flow<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      postConsume: (jobId: string) => Promise<undefined | string>;
                      init(): void;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      on(
                          event: "data",
                          listener: (
                              job: JobRequest<Type, Data, CustomHeaders, CustomOptions>,
                          ) => void,
                      ): this;
                      pause(): void;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  postConsume: (jobId: string) => Promise<undefined | string>

                  Perform the task to clean the job registers after the job has been resolved

                  +

                  Type declaration

                    • (jobId: string): Promise<undefined | string>
                    • Parameters

                      • jobId: string

                        Job entry identification

                        +

                      Returns Promise<undefined | string>

                        +
                      • the job entry identification that has been correctly removed or undefined if the job +was not found
                      • +
                      +

                  Methods

                  • Enable consuming process

                    +

                    Returns void

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Emitted when there is a new job to be managed

                    +

                    Parameters

                    Returns this

                  • Stop consuming process

                    +

                    Returns void

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose.Plugs.Source.Sequence.html b/docs/interfaces/_mdf.js_firehose.Plugs.Source.Sequence.html new file mode 100644 index 00000000..a81b64e9 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.Plugs.Source.Sequence.html @@ -0,0 +1,24 @@ +Sequence | @mdf.js

                  Interface Sequence<Type, Data, CustomHeaders, CustomOptions>

                  Sequence Source interface definition +A Sequence is a Source that allows to manage the flow of Jobs using the ingestData method to control +the rate of Jobs that can be processed

                  +
                  interface Sequence<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      postConsume: (jobId: string) => Promise<undefined | string>;
                      ingestData(
                          size: number,
                      ): Promise<
                          | JobRequest<Type, Data, CustomHeaders, CustomOptions>
                          | JobRequest<Type, Data, CustomHeaders, CustomOptions>[],
                      >;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      on(
                          event: "data",
                          listener: (
                              job: JobRequest<Type, Data, CustomHeaders, CustomOptions>,
                          ) => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  postConsume: (jobId: string) => Promise<undefined | string>

                  Perform the task to clean the job registers after the job has been resolved

                  +

                  Type declaration

                    • (jobId: string): Promise<undefined | string>
                    • Parameters

                      • jobId: string

                        Job entry identification

                        +

                      Returns Promise<undefined | string>

                        +
                      • the job entry identification that has been correctly removed or undefined if the job +was not found
                      • +
                      +

                  Methods

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Emitted when there is a new job to be managed

                    +

                    Parameters

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose.PostConsumeOptions.html b/docs/interfaces/_mdf.js_firehose.PostConsumeOptions.html new file mode 100644 index 00000000..b3f7f50d --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose.PostConsumeOptions.html @@ -0,0 +1,8 @@ +PostConsumeOptions | @mdf.js

                  Interface PostConsumeOptions

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface PostConsumeOptions {
                      checkUncleanedInterval?: number;
                      maxUnknownJobs?: number;
                  }

                  Properties

                  checkUncleanedInterval?: number

                  Time to wait between check buffer of uncleaned entries

                  +
                  maxUnknownJobs?: number

                  Number of unknown jobs to register

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.Base-2.html b/docs/interfaces/_mdf.js_firehose._internal_.Base-2.html new file mode 100644 index 00000000..ab39092a --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.Base-2.html @@ -0,0 +1,19 @@ +Base | @mdf.js

                  Interface Base<Type, Data, CustomHeaders, CustomOptions>

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface Base<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = NoMoreHeaders,
                      CustomOptions extends Record<string, any> = NoMoreOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      single: (
                          job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                      ) => Promise<void>;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = NoMoreHeaders
                  • CustomOptions extends Record<string, any> = NoMoreOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  single: (
                      job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                  ) => Promise<void>

                  Perform the processing of a single Job

                  +

                  Type declaration

                  Methods

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.Base-3.html b/docs/interfaces/_mdf.js_firehose._internal_.Base-3.html new file mode 100644 index 00000000..ef9a57d8 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.Base-3.html @@ -0,0 +1,24 @@ +Base | @mdf.js

                  Interface Base<Type, Data, CustomHeaders, CustomOptions>

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface Base<
                      Type extends string = string,
                      Data = any,
                      CustomHeaders extends Record<string, any> = AnyHeaders,
                      CustomOptions extends Record<string, any> = AnyOptions,
                  > {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      postConsume: (jobId: string) => Promise<undefined | string>;
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      on(
                          event: "data",
                          listener: (
                              job: JobRequest<Type, Data, CustomHeaders, CustomOptions>,
                          ) => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  postConsume: (jobId: string) => Promise<undefined | string>

                  Perform the task to clean the job registers after the job has been resolved

                  +

                  Type declaration

                    • (jobId: string): Promise<undefined | string>
                    • Parameters

                      • jobId: string

                        Job entry identification

                        +

                      Returns Promise<undefined | string>

                        +
                      • the job entry identification that has been correctly removed or undefined if the job +was not found
                      • +
                      +

                  Methods

                  • Emitted when the component throw an error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every status change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Emitted when there is a new job to be managed

                    +

                    Parameters

                    Returns this

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.EngineOptions.html b/docs/interfaces/_mdf.js_firehose._internal_.EngineOptions.html new file mode 100644 index 00000000..6068a424 --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.EngineOptions.html @@ -0,0 +1,9 @@ +EngineOptions | @mdf.js
                  interface EngineOptions {
                      logger?: LoggerInstance;
                      maxInactivityTime?: number;
                      strategies?: { [type: string]: OpenStrategy[] };
                      transformOptions?: TransformOptions;
                  }

                  Properties

                  Debug logger for development and deep troubleshooting

                  +
                  maxInactivityTime?: number

                  Maximum time of inactivity before the firehose notify that is hold

                  +
                  strategies?: { [type: string]: OpenStrategy[] }

                  Strategies to be applied over the jobs

                  +
                  transformOptions?: TransformOptions

                  Transform streams options

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.SinkOptions.html b/docs/interfaces/_mdf.js_firehose._internal_.SinkOptions.html new file mode 100644 index 00000000..e43420da --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.SinkOptions.html @@ -0,0 +1,7 @@ +SinkOptions | @mdf.js
                  interface SinkOptions {
                      logger?: LoggerInstance;
                      retryOptions?: RetryOptions;
                      writableOptions?: WritableOptions;
                  }

                  Properties

                  Debug logger for development and deep troubleshooting

                  +
                  retryOptions?: RetryOptions

                  Options for job retry operations

                  +
                  writableOptions?: WritableOptions

                  Writable streams options

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.SourceOptions.html b/docs/interfaces/_mdf.js_firehose._internal_.SourceOptions.html new file mode 100644 index 00000000..a9fd391a --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.SourceOptions.html @@ -0,0 +1,12 @@ +SourceOptions | @mdf.js
                  interface SourceOptions {
                      logger?: LoggerInstance;
                      postConsumeOptions?: PostConsumeOptions;
                      qos?: number;
                      readableOptions?: ReadableOptions;
                      retryOptions?: RetryOptions;
                  }

                  Properties

                  Debug logger for development and deep troubleshooting

                  +
                  postConsumeOptions?: PostConsumeOptions

                  Post consume operations options

                  +
                  qos?: number

                  Indicates the quality of service for the job, indeed this indicate the number of sinks that +must be successfully processed to consider the job as successfully processed

                  +
                  readableOptions?: ReadableOptions

                  Readable streams options

                  +
                  retryOptions?: RetryOptions

                  Options for job retry operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.WrappableSinkPlug.html b/docs/interfaces/_mdf.js_firehose._internal_.WrappableSinkPlug.html new file mode 100644 index 00000000..5617aa0a --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.WrappableSinkPlug.html @@ -0,0 +1,19 @@ +WrappableSinkPlug | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface WrappableSinkPlug {
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      multi?: (jobs: OpenJobObject[]) => Promise<void>;
                      single: (job: OpenJobObject) => Promise<void>;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Hierarchy (View Summary)

                  Properties

                  Methods

                  Properties

                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  multi?: (jobs: OpenJobObject[]) => Promise<void>

                  Perform the processing of several Jobs

                  +

                  Type declaration

                  single: (job: OpenJobObject) => Promise<void>

                  Perform the processing of a single Job

                  +

                  Type declaration

                  Methods

                  • Start the Plug and the underlayer resources, making it available

                    +

                    Returns Promise<void>

                  • Stop the Plug and the underlayer resources, making it unavailable

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_firehose._internal_.WrappableSourcePlug.html b/docs/interfaces/_mdf.js_firehose._internal_.WrappableSourcePlug.html new file mode 100644 index 00000000..c674f88b --- /dev/null +++ b/docs/interfaces/_mdf.js_firehose._internal_.WrappableSourcePlug.html @@ -0,0 +1,46 @@ +WrappableSourcePlug | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface WrappableSourcePlug {
                      addCredits?: (credits: number) => Promise<number>;
                      ingestData?: (size: number) => Promise<OpenJobRequest | OpenJobRequest[]>;
                      init?: () => void;
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      pause?: () => void;
                      postConsume: (jobId: string) => Promise<undefined | string>;
                      start: () => Promise<void>;
                      stop: () => Promise<void>;
                      on(
                          event: "error" | "status" | "data",
                          listener: (...args: any[]) => void,
                      ): this;
                  }

                  Hierarchy (View Summary)

                  Properties

                  addCredits?: (credits: number) => Promise<number>

                  Add new credits to the source

                  +

                  Type declaration

                    • (credits: number): Promise<number>
                    • Parameters

                      • credits: number

                        Credits to be added to the source

                        +

                      Returns Promise<number>

                  ingestData?: (size: number) => Promise<OpenJobRequest | OpenJobRequest[]>

                  Perform the ingestion of new jobs

                  +

                  Type declaration

                  init?: () => void

                  Enable consuming process

                  +
                  metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                  Metrics registry for this component

                  +
                  pause?: () => void

                  Stop consuming process

                  +
                  postConsume: (jobId: string) => Promise<undefined | string>

                  Perform the task to clean the job registers after the job has been resolved

                  +

                  Type declaration

                    • (jobId: string): Promise<undefined | string>
                    • Parameters

                      • jobId: string

                        Job entry identification

                        +

                      Returns Promise<undefined | string>

                        +
                      • the job entry identification that has been correctly removed or undefined if the job +was not found
                      • +
                      +
                  start: () => Promise<void>

                  Start the Plug and the underlayer resources, making it available

                  +
                  stop: () => Promise<void>

                  Stop the Plug and the underlayer resources, making it unavailable

                  +

                  Methods

                  • Adds the listener function to the end of the listeners array for the event +named eventName. No checks are made to see if the listener has already +been added. Multiple calls passing the same combination of eventName and +listener will result in the listener being added, and called, multiple times.

                    +
                    server.on('connection', (stream) => {
                    console.log('someone connected!');
                    }); +
                    + +

                    Returns a reference to the EventEmitter, so that calls can be chained.

                    +

                    By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the +event listener to the beginning of the listeners array.

                    +
                    import { EventEmitter } from 'node:events';
                    const myEE = new EventEmitter();
                    myEE.on('foo', () => console.log('a'));
                    myEE.prependListener('foo', () => console.log('b'));
                    myEE.emit('foo');
                    // Prints:
                    // b
                    // a +
                    + +

                    Parameters

                    • event: "error" | "status" | "data"
                    • listener: (...args: any[]) => void

                      The callback function

                      +

                    Returns this

                    v0.1.101

                    +
                  diff --git a/docs/interfaces/_mdf.js_http-client-provider.HTTP.Config.html b/docs/interfaces/_mdf.js_http-client-provider.HTTP.Config.html new file mode 100644 index 00000000..2312b480 --- /dev/null +++ b/docs/interfaces/_mdf.js_http-client-provider.HTTP.Config.html @@ -0,0 +1,4 @@ +Config | @mdf.js
                  interface Config {
                      httpAgentOptions?: AgentOptions;
                      httpsAgentOptions?: AgentOptions;
                      requestConfig?: CreateAxiosDefaults<any>;
                  }

                  Properties

                  httpAgentOptions?: AgentOptions
                  httpsAgentOptions?: AgentOptions
                  requestConfig?: CreateAxiosDefaults<any>
                  diff --git a/docs/interfaces/_mdf.js_http-server-provider.HTTP.Config.html b/docs/interfaces/_mdf.js_http-server-provider.HTTP.Config.html new file mode 100644 index 00000000..d022ab04 --- /dev/null +++ b/docs/interfaces/_mdf.js_http-server-provider.HTTP.Config.html @@ -0,0 +1,8 @@ +Config | @mdf.js

                  Configuration for the HTTP server provider

                  +
                  interface Config {
                      app?: Express;
                      host?: string;
                      port?: number;
                  }

                  Properties

                  Properties

                  app?: Express

                  Express app to use

                  +
                  host?: string

                  Host to listen on

                  +
                  port?: number

                  Port to listen on

                  +
                  diff --git a/docs/interfaces/_mdf.js_jsonl-archiver.AppendResult.html b/docs/interfaces/_mdf.js_jsonl-archiver.AppendResult.html new file mode 100644 index 00000000..3d359e35 --- /dev/null +++ b/docs/interfaces/_mdf.js_jsonl-archiver.AppendResult.html @@ -0,0 +1,13 @@ +AppendResult | @mdf.js
                  interface AppendResult {
                      appended: number;
                      errorRecords: { error: Crash; record: Record<string, unknown> }[];
                      errors: number;
                      skipped: number;
                      skippedRecords: Record<string, unknown>[];
                      success: boolean;
                  }

                  Properties

                  appended: number

                  Number of records appended

                  +
                  errorRecords: { error: Crash; record: Record<string, unknown> }[]

                  Records that triggered an error

                  +
                  errors: number

                  Number of records that triggered an error

                  +
                  skipped: number

                  Number of records that were skipped

                  +
                  skippedRecords: Record<string, unknown>[]

                  Records that were skipped

                  +
                  success: boolean

                  Indicates if the operation was completed successfully

                  +
                  diff --git a/docs/interfaces/_mdf.js_jsonl-archiver.ArchiveOptions.html b/docs/interfaces/_mdf.js_jsonl-archiver.ArchiveOptions.html new file mode 100644 index 00000000..1bfc3fa5 --- /dev/null +++ b/docs/interfaces/_mdf.js_jsonl-archiver.ArchiveOptions.html @@ -0,0 +1,129 @@ +ArchiveOptions | @mdf.js

                  Represents the options for the JsonlFileStoreManager

                  +
                  interface ArchiveOptions {
                      archiveFolderPath: string;
                      createFolders: boolean;
                      defaultBaseFilename?: string;
                      fileEncoding: BufferEncoding;
                      inactiveTimeout?: number;
                      logger?: LoggerInstance;
                      propertyData?: string;
                      propertyFileName?: string;
                      propertySkip?: string;
                      propertySkipValue?: string | number | boolean;
                      retryOptions?: RetryOptions;
                      rotationInterval?: number;
                      rotationLines?: number;
                      rotationSize?: number;
                      separator?: string;
                      workingFolderPath: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  archiveFolderPath: string

                  Path to the folder where the closed files are stored

                  +
                  './data/archive'
                  +
                  + +
                  './data/archive'
                  +
                  + +
                  createFolders: boolean

                  If true, it will create the folders if they don't exist

                  +
                  true
                  +
                  + +
                  true
                  +
                  + +
                  defaultBaseFilename?: string

                  Base filename for the files

                  +
                  'file'
                  +
                  + +
                  'file'
                  +
                  + +
                  fileEncoding: BufferEncoding

                  Encoding to use when writing to files

                  +
                  'utf-8'
                  +
                  + +
                  inactiveTimeout?: number

                  Maximum inactivity time in milliseconds before a handler is cleaned up

                  +
                  60000
                  +
                  + +
                  undefined
                  +
                  + +

                  Logger instance to use

                  +
                  undefined
                  +
                  + +
                  propertyData?: string

                  If set, this property will be used to store the data in the file, it could be a nested property +in the data object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertyFileName?: string

                  If set, this property will be used as the filename, it could be a nested property in the data +object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertySkip?: string

                  If set, this property will be used to skip the data, it could be a nested property in the data +object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertySkipValue?: string | number | boolean

                  If set, this value will be used to skip the data, it could be a string, number or boolean. If +value is not set, but propertySkip is set, a not falsy value will be used to skip the data, +this means that any value that is not false, 0 or '' will be used to skip the data.

                  +
                  'skip' | 0 | false
                  +
                  + +
                  undefined
                  +
                  + +
                  retryOptions?: RetryOptions

                  Retry options for the file handler operations

                  +
                  { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                  +
                  + +
                  { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                  +
                  + +
                  rotationInterval?: number

                  Interval in milliseconds to rotate the file

                  +
                  3600000 (1 hour)
                  +
                  + +
                  undefined
                  +
                  + +
                  rotationLines?: number

                  Max number of lines before rotating the file

                  +
                  10000 (10k lines)
                  +
                  + +
                  undefined
                  +
                  + +
                  rotationSize?: number

                  Max size of the file before rotating it

                  +
                  10485760 (10 MB)
                  +
                  + +
                  undefined
                  +
                  + +
                  separator?: string

                  Separator to use when writing the data to the file

                  +
                  '\n'
                  +
                  + +
                  '\n'
                  +
                  + +
                  workingFolderPath: string

                  Path to the folder where the working files are stored

                  +
                  './data/working'
                  +
                  + +
                  './data/working'
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_jsonl-archiver.FileStats.html b/docs/interfaces/_mdf.js_jsonl-archiver.FileStats.html new file mode 100644 index 00000000..4239ff20 --- /dev/null +++ b/docs/interfaces/_mdf.js_jsonl-archiver.FileStats.html @@ -0,0 +1,30 @@ +FileStats | @mdf.js

                  Represents the statistics of a jsonl file

                  +
                  interface FileStats {
                      appendErrors: number;
                      appendSuccesses: number;
                      creationTimestamp: string;
                      currentSize: number;
                      fileName: string;
                      filePath: string;
                      handler: string;
                      isActive: boolean;
                      lastError?: Crash;
                      lastModifiedTimestamp: string;
                      lastRotationTimestamp: string;
                      numberLines: number;
                      onError: boolean;
                      rotationCount: number;
                  }

                  Properties

                  appendErrors: number

                  Number of append errors

                  +
                  appendSuccesses: number

                  Number of append successes in active file

                  +
                  creationTimestamp: string

                  Creation timestamp

                  +
                  currentSize: number

                  Current size in bytes

                  +
                  fileName: string

                  File name

                  +
                  filePath: string

                  File path

                  +
                  handler: string

                  File handler

                  +
                  isActive: boolean

                  Flag to indicate if the file is active

                  +
                  lastError?: Crash

                  Last error

                  +
                  lastModifiedTimestamp: string

                  Last modified timestamp

                  +
                  lastRotationTimestamp: string

                  Last rotation timestamp

                  +
                  numberLines: number

                  Number of lines

                  +
                  onError: boolean

                  Flag to indicate if the last operation was an error

                  +
                  rotationCount: number

                  Number of rotations

                  +
                  diff --git a/docs/interfaces/_mdf.js_jsonl-archiver._internal_.FileHandlerOptions.html b/docs/interfaces/_mdf.js_jsonl-archiver._internal_.FileHandlerOptions.html new file mode 100644 index 00000000..94049270 --- /dev/null +++ b/docs/interfaces/_mdf.js_jsonl-archiver._internal_.FileHandlerOptions.html @@ -0,0 +1,131 @@ +FileHandlerOptions | @mdf.js

                  Represents the options for the SingleJsonlFileManager

                  +
                  interface FileHandlerOptions {
                      archiveFolderPath: string;
                      baseFilename: string;
                      createFolders: boolean;
                      defaultBaseFilename?: string;
                      fileEncoding: BufferEncoding;
                      inactiveTimeout?: number;
                      logger?: LoggerInstance;
                      propertyData?: string;
                      propertyFileName?: string;
                      propertySkip?: string;
                      propertySkipValue?: string | number | boolean;
                      retryOptions?: RetryOptions;
                      rotationInterval?: number;
                      rotationLines?: number;
                      rotationSize?: number;
                      separator?: string;
                      workingFolderPath: string;
                  }

                  Hierarchy (View Summary)

                  Properties

                  archiveFolderPath: string

                  Path to the folder where the closed files are stored

                  +
                  './data/archive'
                  +
                  + +
                  './data/archive'
                  +
                  + +
                  baseFilename: string

                  Base filename for the files

                  +
                  createFolders: boolean

                  If true, it will create the folders if they don't exist

                  +
                  true
                  +
                  + +
                  true
                  +
                  + +
                  defaultBaseFilename?: string

                  Base filename for the files

                  +
                  'file'
                  +
                  + +
                  'file'
                  +
                  + +
                  fileEncoding: BufferEncoding

                  Encoding to use when writing to files

                  +
                  'utf-8'
                  +
                  + +
                  inactiveTimeout?: number

                  Maximum inactivity time in milliseconds before a handler is cleaned up

                  +
                  60000
                  +
                  + +
                  undefined
                  +
                  + +

                  Logger instance to use

                  +
                  undefined
                  +
                  + +
                  propertyData?: string

                  If set, this property will be used to store the data in the file, it could be a nested property +in the data object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertyFileName?: string

                  If set, this property will be used as the filename, it could be a nested property in the data +object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertySkip?: string

                  If set, this property will be used to skip the data, it could be a nested property in the data +object expressed as a dot separated string

                  +
                  'data.property'
                  +
                  + +
                  undefined
                  +
                  + +
                  propertySkipValue?: string | number | boolean

                  If set, this value will be used to skip the data, it could be a string, number or boolean. If +value is not set, but propertySkip is set, a not falsy value will be used to skip the data, +this means that any value that is not false, 0 or '' will be used to skip the data.

                  +
                  'skip' | 0 | false
                  +
                  + +
                  undefined
                  +
                  + +
                  retryOptions?: RetryOptions

                  Retry options for the file handler operations

                  +
                  { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                  +
                  + +
                  { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                  +
                  + +
                  rotationInterval?: number

                  Interval in milliseconds to rotate the file

                  +
                  3600000 (1 hour)
                  +
                  + +
                  undefined
                  +
                  + +
                  rotationLines?: number

                  Max number of lines before rotating the file

                  +
                  10000 (10k lines)
                  +
                  + +
                  undefined
                  +
                  + +
                  rotationSize?: number

                  Max size of the file before rotating it

                  +
                  10485760 (10 MB)
                  +
                  + +
                  undefined
                  +
                  + +
                  separator?: string

                  Separator to use when writing the data to the file

                  +
                  '\n'
                  +
                  + +
                  '\n'
                  +
                  + +
                  workingFolderPath: string

                  Path to the folder where the working files are stored

                  +
                  './data/working'
                  +
                  + +
                  './data/working'
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_kafka-provider.Consumer.Config.html b/docs/interfaces/_mdf.js_kafka-provider.Consumer.Config.html new file mode 100644 index 00000000..59ffd27c --- /dev/null +++ b/docs/interfaces/_mdf.js_kafka-provider.Consumer.Config.html @@ -0,0 +1,7 @@ +Config | @mdf.js
                  interface Config {
                      client: KafkaConfig;
                      consumer: ConsumerConfig;
                      interval?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  Properties

                  client: KafkaConfig

                  Kafka client configuration options

                  +
                  consumer: ConsumerConfig

                  Kafka consumer configuration options

                  +
                  interval?: number

                  Period of health check interval

                  +
                  diff --git a/docs/interfaces/_mdf_js_kafka_provider.Producer.Config.html b/docs/interfaces/_mdf.js_kafka-provider.Producer.Config.html similarity index 54% rename from docs/interfaces/_mdf_js_kafka_provider.Producer.Config.html rename to docs/interfaces/_mdf.js_kafka-provider.Producer.Config.html index 83252442..687d0dcb 100644 --- a/docs/interfaces/_mdf_js_kafka_provider.Producer.Config.html +++ b/docs/interfaces/_mdf.js_kafka-provider.Producer.Config.html @@ -1,7 +1,7 @@ -Config | @mdf.js
                  interface Config {
                      client: KafkaConfig;
                      interval?: number;
                      producer?: ProducerConfig;
                  }

                  Properties

                  client -interval? -producer? +Config | @mdf.js
                  interface Config {
                      client: KafkaConfig;
                      interval?: number;
                      producer?: ProducerConfig;
                  }

                  Properties

                  client: KafkaConfig

                  Kafka client configuration options

                  interval?: number

                  Period of health check interval

                  producer?: ProducerConfig

                  Kafka producer configuration options

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_kafka-provider._internal_.BaseConfig.html b/docs/interfaces/_mdf.js_kafka-provider._internal_.BaseConfig.html new file mode 100644 index 00000000..83d2613c --- /dev/null +++ b/docs/interfaces/_mdf.js_kafka-provider._internal_.BaseConfig.html @@ -0,0 +1,5 @@ +BaseConfig | @mdf.js
                  interface BaseConfig {
                      client: KafkaConfig;
                      interval?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  Properties

                  client: KafkaConfig

                  Kafka client configuration options

                  +
                  interval?: number

                  Period of health check interval

                  +
                  diff --git a/docs/interfaces/_mdf_js_logger.ConsoleTransportConfig.html b/docs/interfaces/_mdf.js_logger.ConsoleTransportConfig.html similarity index 53% rename from docs/interfaces/_mdf_js_logger.ConsoleTransportConfig.html rename to docs/interfaces/_mdf.js_logger.ConsoleTransportConfig.html index a056ced6..81bbd44f 100644 --- a/docs/interfaces/_mdf_js_logger.ConsoleTransportConfig.html +++ b/docs/interfaces/_mdf.js_logger.ConsoleTransportConfig.html @@ -1,6 +1,6 @@ -ConsoleTransportConfig | @mdf.js

                  Interface ConsoleTransportConfig

                  Console transport configuration

                  -
                  interface ConsoleTransportConfig {
                      enabled?: boolean;
                      level?: string;
                  }

                  Properties

                  enabled? -level? +ConsoleTransportConfig | @mdf.js

                  Interface ConsoleTransportConfig

                  Console transport configuration

                  +
                  interface ConsoleTransportConfig {
                      enabled?: boolean;
                      level?: string;
                  }

                  Properties

                  Properties

                  enabled?: boolean

                  Console transport enabled, default: false

                  level?: string

                  Console log level, default: info

                  -
                  +
                  diff --git a/docs/interfaces/_mdf_js_logger.FileTransportConfig.html b/docs/interfaces/_mdf.js_logger.FileTransportConfig.html similarity index 57% rename from docs/interfaces/_mdf_js_logger.FileTransportConfig.html rename to docs/interfaces/_mdf.js_logger.FileTransportConfig.html index 9be81bec..f3f3201d 100644 --- a/docs/interfaces/_mdf_js_logger.FileTransportConfig.html +++ b/docs/interfaces/_mdf.js_logger.FileTransportConfig.html @@ -1,16 +1,16 @@ -FileTransportConfig | @mdf.js

                  Interface FileTransportConfig

                  File transport configuration interface

                  -
                  interface FileTransportConfig {
                      enabled?: boolean;
                      filename?: string;
                      json?: boolean;
                      level?: string;
                      maxFiles?: number;
                      maxsize?: number;
                      zippedArchive?: boolean;
                  }

                  Properties

                  enabled? -filename? -json? -level? -maxFiles? -maxsize? -zippedArchive? +FileTransportConfig | @mdf.js

                  Interface FileTransportConfig

                  File transport configuration interface

                  +
                  interface FileTransportConfig {
                      enabled?: boolean;
                      filename?: string;
                      json?: boolean;
                      level?: string;
                      maxFiles?: number;
                      maxsize?: number;
                      zippedArchive?: boolean;
                  }

                  Properties

                  enabled?: boolean

                  File transport enabled, default: false

                  filename?: string

                  Log file name, default: logs/netin-app.log

                  json?: boolean

                  Store in JSON format, default: false

                  level?: string

                  File log level, default: info

                  -
                  maxFiles?: number

                  Max number of files, default: 10

                  +
                  maxFiles?: number

                  Max number of files, default: 10

                  maxsize?: number

                  Max file size, default: 10 Mb

                  -
                  zippedArchive?: boolean

                  Store in zipped format, default: false

                  -
                  +
                  zippedArchive?: boolean

                  Store in zipped format, default: false

                  +
                  diff --git a/docs/interfaces/_mdf_js_logger.LoggerConfig.html b/docs/interfaces/_mdf.js_logger.LoggerConfig.html similarity index 51% rename from docs/interfaces/_mdf_js_logger.LoggerConfig.html rename to docs/interfaces/_mdf.js_logger.LoggerConfig.html index f98a68f3..652cbcf1 100644 --- a/docs/interfaces/_mdf_js_logger.LoggerConfig.html +++ b/docs/interfaces/_mdf.js_logger.LoggerConfig.html @@ -1,5 +1,5 @@ -LoggerConfig | @mdf.js

                  Interface LoggerConfig

                  Logger transports configuration interface

                  -
                  interface LoggerConfig {
                      console?: ConsoleTransportConfig;
                      file?: FileTransportConfig;
                      fluentd?: FluentdTransportConfig;
                  }

                  Properties

                  Properties

                  +LoggerConfig | @mdf.js

                  Interface LoggerConfig

                  Logger transports configuration interface

                  +
                  interface LoggerConfig {
                      console?: ConsoleTransportConfig;
                      file?: FileTransportConfig;
                      fluentd?: FluentdTransportConfig;
                  }

                  Properties

                  Properties

                  diff --git a/docs/interfaces/_mdf.js_logger.LoggerInstance.html b/docs/interfaces/_mdf.js_logger.LoggerInstance.html new file mode 100644 index 00000000..7f746b28 --- /dev/null +++ b/docs/interfaces/_mdf.js_logger.LoggerInstance.html @@ -0,0 +1,52 @@ +LoggerInstance | @mdf.js

                  Interface LoggerInstance

                  Represents a logger instance with different log levels and functions.

                  +
                  interface LoggerInstance {
                      crash: (error: Crash | Boom | Multi, context?: string) => void;
                      debug: LoggerFunction;
                      error: LoggerFunction;
                      info: LoggerFunction;
                      silly: LoggerFunction;
                      stream: { write: (message: string) => void };
                      verbose: LoggerFunction;
                      warn: LoggerFunction;
                  }

                  Implemented by

                  Properties

                  crash: (error: Crash | Boom | Multi, context?: string) => void

                  Log events in the ERROR level: all the information in a very detailed way. +This level used to be necessary only in the development process.

                  +

                  Type declaration

                    • (error: Crash | Boom | Multi, context?: string): void
                    • Parameters

                      • error: Crash | Boom | Multi

                        crash error instance

                        +
                      • Optionalcontext: string

                        context (class/function) where this logger is logging

                        +

                      Returns void

                  Log events in the DEBUG level: all the information in a detailed way. +This level used to be necessary only in the debugging process, so not all the data is +reported, only the related with the main processes and tasks.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +

                  Log events in the ERROR level: all the errors and problems with detailed information.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +

                  Log events in the INFO level: only relevant events are reported. +This level is the default level.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +

                  Log events in the SILLY level: all the information in a very detailed way. +This level used to be necessary only in the development process, and the meta data used to be +the results of the operations.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +
                  stream: { write: (message: string) => void }

                  Logs events in the stream.

                  +

                  Log events in the VERBOSE level: trace information without details. +This level used to be necessary only in system configuration process, so information about +the settings and startup process used to be reported.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +

                  Log events in the WARN level: information about possible problems or dangerous situations.

                  +

                  human readable information to log

                  +

                  unique identifier for the actual job/task/request process

                  +

                  context (class/function) where this logger is logging

                  +

                  extra information

                  +
                  diff --git a/docs/interfaces/_mdf_js_middlewares.AuditConfig.html b/docs/interfaces/_mdf.js_middlewares.AuditConfig.html similarity index 51% rename from docs/interfaces/_mdf_js_middlewares.AuditConfig.html rename to docs/interfaces/_mdf.js_middlewares.AuditConfig.html index 7b991246..9c32335d 100644 --- a/docs/interfaces/_mdf_js_middlewares.AuditConfig.html +++ b/docs/interfaces/_mdf.js_middlewares.AuditConfig.html @@ -1,14 +1,14 @@ -AuditConfig | @mdf.js

                  Audit options

                  -
                  interface AuditConfig {
                      area: string;
                      category: AuditCategory;
                      details: string;
                      includeBody?: boolean;
                      includeParams?: boolean;
                      includeQuery?: boolean;
                  }

                  Properties

                  area -category -details -includeBody? -includeParams? -includeQuery? +AuditConfig | @mdf.js

                  Audit options

                  +
                  interface AuditConfig {
                      area: string;
                      category: AuditCategory;
                      details: string;
                      includeBody?: boolean;
                      includeParams?: boolean;
                      includeQuery?: boolean;
                  }

                  Properties

                  area: string

                  Access area

                  -
                  category: AuditCategory

                  Access mode

                  +
                  category: AuditCategory

                  Access mode

                  details: string

                  Access details

                  -
                  includeBody?: boolean

                  Include body

                  -
                  includeParams?: boolean

                  Include params

                  -
                  includeQuery?: boolean

                  Include query

                  -
                  +
                  includeBody?: boolean

                  Include body

                  +
                  includeParams?: boolean

                  Include params

                  +
                  includeQuery?: boolean

                  Include query

                  +
                  diff --git a/docs/interfaces/_mdf.js_middlewares.CacheConfig.html b/docs/interfaces/_mdf.js_middlewares.CacheConfig.html new file mode 100644 index 00000000..ad8aedc7 --- /dev/null +++ b/docs/interfaces/_mdf.js_middlewares.CacheConfig.html @@ -0,0 +1,19 @@ +CacheConfig | @mdf.js

                  Cache options interface

                  +
                  interface CacheConfig {
                      duration: number;
                      enabled: boolean;
                      headersBlacklist: string[];
                      prefixKey: string;
                      statusCodes: { exclude: number[]; include: number[] };
                      toggle: (
                          req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>,
                          res: Response<any, Record<string, any>>,
                      ) => boolean;
                      useBody: boolean;
                  }

                  Properties

                  duration: number

                  Default duration in seconds. Default: 10

                  +
                  enabled: boolean

                  Enablement flag. Default: true

                  +
                  headersBlacklist: string[]

                  List of header that should not be cached

                  +
                  prefixKey: string

                  Prefix key

                  +
                  statusCodes: { exclude: number[]; include: number[] }

                  List of status codes excluded and included

                  +

                  Type declaration

                  • exclude: number[]

                    Specifically Excluded status codes. Default: []

                    +
                  • include: number[]

                    Specifically Included status codes. Default: [200]

                    +
                  toggle: (
                      req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>,
                      res: Response<any, Record<string, any>>,
                  ) => boolean

                  Toggle cache function. Takes the request/objects and must return a boolean value. If true, the +response will be cached

                  +
                  useBody: boolean

                  Use request has as part of cache key

                  +
                  diff --git a/docs/interfaces/_mdf.js_middlewares.CorsConfig.html b/docs/interfaces/_mdf.js_middlewares.CorsConfig.html new file mode 100644 index 00000000..f0b2acc8 --- /dev/null +++ b/docs/interfaces/_mdf.js_middlewares.CorsConfig.html @@ -0,0 +1,14 @@ +CorsConfig | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface CorsConfig {
                      allowAppClients?: boolean;
                      allowHeaders?: string[];
                      credentials?: boolean;
                      enabled: boolean;
                      exposedHeaders?: string[];
                      maxAge?: number;
                      methods?: string[];
                      optionsSuccessStatus?: 200 | 204;
                      preflightContinue?: boolean;
                      whitelist?: string | (string | RegExp)[];
                  }

                  Properties

                  allowAppClients?: boolean
                  allowHeaders?: string[]
                  credentials?: boolean
                  enabled: boolean
                  exposedHeaders?: string[]
                  maxAge?: number
                  methods?: string[]
                  optionsSuccessStatus?: 200 | 204
                  preflightContinue?: boolean
                  whitelist?: string | (string | RegExp)[]
                  diff --git a/docs/interfaces/_mdf.js_middlewares.RateLimitConfig.html b/docs/interfaces/_mdf.js_middlewares.RateLimitConfig.html new file mode 100644 index 00000000..f6866d62 --- /dev/null +++ b/docs/interfaces/_mdf.js_middlewares.RateLimitConfig.html @@ -0,0 +1,6 @@ +RateLimitConfig | @mdf.js

                  Rate limit configuration

                  +
                  interface RateLimitConfig {
                      enabled: boolean;
                      rates: RateLimitEntry[];
                  }

                  Properties

                  Properties

                  enabled: boolean

                  Enable rate limiting

                  +

                  Rate limits

                  +
                  diff --git a/docs/interfaces/_mdf.js_middlewares.RateLimitEntry.html b/docs/interfaces/_mdf.js_middlewares.RateLimitEntry.html new file mode 100644 index 00000000..0a2351b0 --- /dev/null +++ b/docs/interfaces/_mdf.js_middlewares.RateLimitEntry.html @@ -0,0 +1,4 @@ +RateLimitEntry | @mdf.js

                  Rate limit entry

                  +

                  Indexable

                  • [x: string]: { maxRequests: number; timeWindow: number }
                    • maxRequests: number

                      Maximum number of requests

                      +
                    • timeWindow: number

                      Time window in seconds

                      +
                  diff --git a/docs/interfaces/_mdf.js_mongo-provider.Mongo.Collections.html b/docs/interfaces/_mdf.js_mongo-provider.Mongo.Collections.html new file mode 100644 index 00000000..45370132 --- /dev/null +++ b/docs/interfaces/_mdf.js_mongo-provider.Mongo.Collections.html @@ -0,0 +1 @@ +Collections | @mdf.js

                  Indexable

                  • [key: string]: { indexes: IndexDescription[]; options: CreateCollectionOptions }
                  diff --git a/docs/interfaces/_mdf_js_mongo_provider.Mongo.Config.html b/docs/interfaces/_mdf.js_mongo-provider.Mongo.Config.html similarity index 50% rename from docs/interfaces/_mdf_js_mongo_provider.Mongo.Config.html rename to docs/interfaces/_mdf.js_mongo-provider.Mongo.Config.html index a7dbcfb6..e796700f 100644 --- a/docs/interfaces/_mdf_js_mongo_provider.Mongo.Config.html +++ b/docs/interfaces/_mdf.js_mongo-provider.Mongo.Config.html @@ -1,5 +1,5 @@ -Config | @mdf.js
                  interface Config {
                      collections?: Collections;
                      url?: string;
                  }

                  Hierarchy

                  • MongoClientOptions
                    • Config

                  Properties

                  Properties

                  collections?: Collections

                  Mongo database collections

                  +Config | @mdf.js
                  interface Config {
                      collections?: Collections;
                      url?: string;
                  }

                  Hierarchy

                  • MongoClientOptions
                    • Config

                  Properties

                  Properties

                  collections?: Collections

                  Mongo database collections

                  url?: string

                  Mongo database connection string

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_mqtt-provider.MQTT.Config.html b/docs/interfaces/_mdf.js_mqtt-provider.MQTT.Config.html new file mode 100644 index 00000000..db2e8ffb --- /dev/null +++ b/docs/interfaces/_mdf.js_mqtt-provider.MQTT.Config.html @@ -0,0 +1,3 @@ +Config | @mdf.js
                  interface Config {
                      url?: string;
                  }

                  Hierarchy

                  • IClientOptions
                    • Config

                  Properties

                  Properties

                  url?: string

                  MQTT broker url

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.CommandJobHeader.html b/docs/interfaces/_mdf.js_openc2-core.CommandJobHeader.html new file mode 100644 index 00000000..4c853289 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.CommandJobHeader.html @@ -0,0 +1,3 @@ +CommandJobHeader | @mdf.js
                  interface CommandJobHeader {
                      duration: number;
                  }

                  Properties

                  Properties

                  duration: number

                  Delay allowed from command message

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.ConsumerAdapter.html b/docs/interfaces/_mdf.js_openc2-core.ConsumerAdapter.html new file mode 100644 index 00000000..015624ff --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.ConsumerAdapter.html @@ -0,0 +1,20 @@ +ConsumerAdapter | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface ConsumerAdapter {
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                      subscribe(handler: OnCommandHandler): Promise<void>;
                      unsubscribe(handler: OnCommandHandler): Promise<void>;
                  }

                  Hierarchy (View Summary)

                  Implemented by

                  Methods

                  • Emitted when a adapter's operation has some error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every state change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  • Subscribe the incoming command handler to the underlayer transport system

                    +

                    Parameters

                    Returns Promise<void>

                  • Unsubscribe the incoming command handler from the underlayer transport system

                    +

                    Parameters

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_openc2-core.ConsumerOptions.html b/docs/interfaces/_mdf.js_openc2-core.ConsumerOptions.html new file mode 100644 index 00000000..22c4856f --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.ConsumerOptions.html @@ -0,0 +1,21 @@ +ConsumerOptions | @mdf.js
                  interface ConsumerOptions {
                      actionTargetPairs: ActionTargetPairs;
                      actuator?: string[];
                      id: string;
                      logger?: LoggerInstance;
                      maxInactivityTime?: number;
                      profiles?: string[];
                      registerLimit?: number;
                      registry?: Registry;
                      resolver?: ResolverMap;
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  actionTargetPairs: ActionTargetPairs

                  Supported pairs Action-Target pairs

                  +
                  actuator?: string[]

                  Actuator

                  +
                  id: string

                  Instance identification

                  +

                  Logger instance

                  +
                  maxInactivityTime?: number

                  Register max inactivity time

                  +
                  profiles?: string[]

                  Supported profiles

                  +
                  registerLimit?: number

                  Maximum number of message to be stored

                  +
                  registry?: Registry

                  Message and jobs registry

                  +
                  resolver?: ResolverMap

                  Resolver

                  +
                  retryOptions?: RetryOptions

                  Options for adapter retry operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.Control.Actuator.html b/docs/interfaces/_mdf.js_openc2-core.Control.Actuator.html new file mode 100644 index 00000000..ed1be7f8 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Actuator.html @@ -0,0 +1,2 @@ +Actuator | @mdf.js

                  Domain based actuator identification

                  +

                  Indexable

                  • [domain: string]: Record<string, any>
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Arguments.html b/docs/interfaces/_mdf.js_openc2-core.Control.Arguments.html similarity index 55% rename from docs/interfaces/_mdf_js_openc2_core.Control.Arguments.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Arguments.html index 40210be3..0813460c 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Arguments.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Arguments.html @@ -1,4 +1,4 @@ -Arguments | @mdf.js

                  start_time, stop_time, duration:

                  +Arguments | @mdf.js

                  start_time, stop_time, duration:

                  • If none are specified, then start_time is now, stop_time is never, and duration is infinity.
                  • Only two of the three are allowed on any given Command and the third is derived from the @@ -19,12 +19,12 @@
                  • If response_requested is not explicitly specified then the Consumer SHOULD respond as if complete was specified.
                  -
                  interface Arguments {
                      duration?: number;
                      response_requested?: ResponseType;
                      start_time?: number;
                      stop_time?: number;
                  }

                  Properties

                  interface Arguments {
                      duration?: number;
                      response_requested?: ResponseType;
                      start_time?: number;
                      stop_time?: number;
                  }

                  Properties

                  duration?: number

                  The length of time for an Command to be in effect

                  -
                  response_requested?: ResponseType

                  The type of Response required for the Command: none, ack, status, complete

                  -
                  start_time?: number

                  The specific date/time to initiate the Command

                  -
                  stop_time?: number

                  The specific date/time to terminate the Command

                  -
                  +
                  response_requested?: ResponseType

                  The type of Response required for the Command: none, ack, status, complete

                  +
                  start_time?: number

                  The specific date/time to initiate the Command

                  +
                  stop_time?: number

                  The specific date/time to terminate the Command

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Artifact.html b/docs/interfaces/_mdf.js_openc2-core.Control.Artifact.html similarity index 51% rename from docs/interfaces/_mdf_js_openc2_core.Control.Artifact.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Artifact.html index 3bd11900..a45d78e2 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Artifact.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Artifact.html @@ -1,8 +1,8 @@ -Artifact | @mdf.js

                  File-like object or a link to that object

                  -
                  interface Artifact {
                      hashes?: Hashes;
                      mime_type?: string;
                      payload?: Payload;
                  }

                  Properties

                  hashes?: Hashes

                  Hashes of the payload content

                  -
                  mime_type?: string

                  Permitted values specified in the IANA Media Types registry, [RFC6838]

                  -
                  payload?: Payload

                  Choice of literal content or URL

                  -
                  +Artifact | @mdf.js

                  File-like object or a link to that object

                  +
                  interface Artifact {
                      hashes?: Hashes;
                      mime_type?: string;
                      payload?: Payload;
                  }

                  Properties

                  hashes?: Hashes

                  Hashes of the payload content

                  +
                  mime_type?: string

                  Permitted values specified in the IANA Media Types registry, [RFC6838]

                  +
                  payload?: Payload

                  Choice of literal content or URL

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Command.html b/docs/interfaces/_mdf.js_openc2-core.Control.Command.html similarity index 55% rename from docs/interfaces/_mdf_js_openc2_core.Control.Command.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Command.html index e71e6a21..49dc5efa 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Command.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Command.html @@ -1,12 +1,12 @@ -Command | @mdf.js

                  Command object

                  -
                  interface Command {
                      action: Action;
                      actuator?: Actuator;
                      args?: Arguments;
                      command_id?: string;
                      target: Target;
                  }

                  Properties

                  action: Action

                  The task or activity to be performed (i.e., the 'verb')

                  -
                  actuator?: Actuator

                  The subject of the Action. The Actuator executes the Action on the Target

                  -
                  args?: Arguments

                  Additional information that applies to the Command

                  -
                  command_id?: string

                  An identifier of this Command

                  -
                  target: Target

                  The object of the Action. The Action is performed on the Target

                  -
                  +Command | @mdf.js

                  Command object

                  +
                  interface Command {
                      action: Action;
                      actuator?: Actuator;
                      args?: Arguments;
                      command_id?: string;
                      target: Target;
                  }

                  Properties

                  action: Action

                  The task or activity to be performed (i.e., the 'verb')

                  +
                  actuator?: Actuator

                  The subject of the Action. The Actuator executes the Action on the Target

                  +
                  args?: Arguments

                  Additional information that applies to the Command

                  +
                  command_id?: string

                  An identifier of this Command

                  +
                  target: Target

                  The object of the Action. The Action is performed on the Target

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.Control.CommandMessage.html b/docs/interfaces/_mdf.js_openc2-core.Control.CommandMessage.html new file mode 100644 index 00000000..b6ec76cf --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.Control.CommandMessage.html @@ -0,0 +1,19 @@ +CommandMessage | @mdf.js
                  interface CommandMessage {
                      content: Command;
                      content_type: string;
                      created: number;
                      from: string;
                      msg_type: Command;
                      request_id: string;
                      to: string[];
                  }

                  Hierarchy (View Summary)

                  Properties

                  content: Command

                  Message body as specified by content_type and msg_type

                  +
                  content_type: string

                  Media Type that identifies the format of the content, including major version. Incompatible +content formats must have different content_types. Content_type application/openc2 identifies +content defined by OpenC2 language specification versions 1.x, i.e., all versions that are +compatible with version 1.0.

                  +
                  created: number

                  Creation date/time of the content

                  +
                  from: string

                  Authenticated identifier of the creator of or authority for execution of a message

                  +
                  msg_type: Command

                  The type of command and control Message

                  +
                  request_id: string

                  A unique identifier created by the Producer and copied by Consumer into all Responses, in order +to support reference to a particular Command, transaction, or event chain

                  +
                  to: string[]

                  Authenticated identifier(s) of the authorized recipient(s) of a message

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Device.html b/docs/interfaces/_mdf.js_openc2-core.Control.Device.html similarity index 50% rename from docs/interfaces/_mdf_js_openc2_core.Control.Device.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Device.html index 311cee0e..dcaefcb3 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Device.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Device.html @@ -1,8 +1,8 @@ -Device | @mdf.js

                  A "Device" Target MUST contain at least one property

                  -
                  interface Device {
                      device_id?: string;
                      hostname?: string;
                      idn_hostname?: string;
                  }

                  Properties

                  device_id?: string

                  An identifier that refers to this device within an inventory or management system

                  +Device | @mdf.js

                  A "Device" Target MUST contain at least one property

                  +
                  interface Device {
                      device_id?: string;
                      hostname?: string;
                      idn_hostname?: string;
                  }

                  Properties

                  device_id?: string

                  An identifier that refers to this device within an inventory or management system

                  hostname?: string

                  A hostname that can be used to connect to this device over a network

                  -
                  idn_hostname?: string

                  An internationalized hostname that can be used to connect to this device over a network

                  -
                  +
                  idn_hostname?: string

                  An internationalized hostname that can be used to connect to this device over a network

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.File.html b/docs/interfaces/_mdf.js_openc2-core.Control.File.html similarity index 55% rename from docs/interfaces/_mdf_js_openc2_core.Control.File.html rename to docs/interfaces/_mdf.js_openc2-core.Control.File.html index defaa9fa..2b63627e 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.File.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.File.html @@ -1,8 +1,8 @@ -File | @mdf.js

                  A "File" Target MUST contain at least one property.

                  -
                  interface File {
                      hashes?: Hashes;
                      name?: string;
                      path?: string;
                  }

                  Properties

                  Properties

                  hashes?: Hashes

                  One or more cryptographic hash codes of the file contents

                  +File | @mdf.js

                  A "File" Target MUST contain at least one property.

                  +
                  interface File {
                      hashes?: Hashes;
                      name?: string;
                      path?: string;
                  }

                  Properties

                  Properties

                  hashes?: Hashes

                  One or more cryptographic hash codes of the file contents

                  name?: string

                  The name of the file as defined in the file system

                  path?: string

                  The absolute path to the location of the file in the file system

                  -
                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Hashes.html b/docs/interfaces/_mdf.js_openc2-core.Control.Hashes.html similarity index 56% rename from docs/interfaces/_mdf_js_openc2_core.Control.Hashes.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Hashes.html index 19225253..e6212ae1 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Hashes.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Hashes.html @@ -1,8 +1,8 @@ -Hashes | @mdf.js

                  One or more cryptographic hash codes of the file contents

                  -
                  interface Hashes {
                      md5?: string;
                      sha1?: string;
                      sha256?: string;
                  }

                  Properties

                  md5? -sha1? -sha256? +Hashes | @mdf.js

                  One or more cryptographic hash codes of the file contents

                  +
                  interface Hashes {
                      md5?: string;
                      sha1?: string;
                      sha256?: string;
                  }

                  Properties

                  Properties

                  md5?: string

                  MD5 hash as defined in [RFC1321]

                  sha1?: string

                  SHA1 hash as defined in [RFC6234]

                  sha256?: string

                  SHA256 hash as defined in [RFC6234]

                  -
                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.IPv6Connection.html b/docs/interfaces/_mdf.js_openc2-core.Control.IPv4Connection.html similarity index 52% rename from docs/interfaces/_mdf_js_openc2_core.Control.IPv6Connection.html rename to docs/interfaces/_mdf.js_openc2-core.Control.IPv4Connection.html index ea07c638..de80ac5d 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.IPv6Connection.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.IPv4Connection.html @@ -1,12 +1,12 @@ -IPv6Connection | @mdf.js

                  IPv6 connection

                  -
                  interface IPv6Connection {
                      dst_addr?: string;
                      dst_port?: number;
                      protocol?: L4Protocol;
                      src_addr?: string;
                      src_port?: number;
                  }

                  Properties

                  dst_addr?: string

                  IPv4 destination address range

                  -
                  dst_port?: number

                  Destination service per [RFC6335]

                  -
                  protocol?: L4Protocol

                  Layer 4 protocol (e.g., TCP)

                  -
                  src_addr?: string

                  IPv4 source address range

                  -
                  src_port?: number

                  Source service per [RFC6335]

                  -
                  +IPv4Connection | @mdf.js

                  IPv4 Connection

                  +
                  interface IPv4Connection {
                      dst_addr?: string;
                      dst_port?: number;
                      protocol?: L4Protocol;
                      src_addr?: string;
                      src_port?: number;
                  }

                  Properties

                  dst_addr?: string

                  IPv4 destination address range

                  +
                  dst_port?: number

                  Destination service per [RFC6335]

                  +
                  protocol?: L4Protocol

                  Layer 4 protocol (e.g., TCP)

                  +
                  src_addr?: string

                  IPv4 source address range

                  +
                  src_port?: number

                  Source service per [RFC6335]

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.IPv4Connection.html b/docs/interfaces/_mdf.js_openc2-core.Control.IPv6Connection.html similarity index 52% rename from docs/interfaces/_mdf_js_openc2_core.Control.IPv4Connection.html rename to docs/interfaces/_mdf.js_openc2-core.Control.IPv6Connection.html index 2a95d3aa..242ec6b5 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.IPv4Connection.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.IPv6Connection.html @@ -1,12 +1,12 @@ -IPv4Connection | @mdf.js

                  IPv4 Connection

                  -
                  interface IPv4Connection {
                      dst_addr?: string;
                      dst_port?: number;
                      protocol?: L4Protocol;
                      src_addr?: string;
                      src_port?: number;
                  }

                  Properties

                  dst_addr?: string

                  IPv4 destination address range

                  -
                  dst_port?: number

                  Destination service per [RFC6335]

                  -
                  protocol?: L4Protocol

                  Layer 4 protocol (e.g., TCP)

                  -
                  src_addr?: string

                  IPv4 source address range

                  -
                  src_port?: number

                  Source service per [RFC6335]

                  -
                  +IPv6Connection | @mdf.js

                  IPv6 connection

                  +
                  interface IPv6Connection {
                      dst_addr?: string;
                      dst_port?: number;
                      protocol?: L4Protocol;
                      src_addr?: string;
                      src_port?: number;
                  }

                  Properties

                  dst_addr?: string

                  IPv4 destination address range

                  +
                  dst_port?: number

                  Destination service per [RFC6335]

                  +
                  protocol?: L4Protocol

                  Layer 4 protocol (e.g., TCP)

                  +
                  src_addr?: string

                  IPv4 source address range

                  +
                  src_port?: number

                  Source service per [RFC6335]

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Payload.html b/docs/interfaces/_mdf.js_openc2-core.Control.Payload.html similarity index 52% rename from docs/interfaces/_mdf_js_openc2_core.Control.Payload.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Payload.html index cc0d8c8f..a2cdf001 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Payload.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Payload.html @@ -1,6 +1,6 @@ -Payload | @mdf.js

                  Choice of literal content or URL

                  -
                  interface Payload {
                      bin?: string;
                      url?: string;
                  }

                  Properties

                  bin? -url? +Payload | @mdf.js

                  Choice of literal content or URL

                  +
                  interface Payload {
                      bin?: string;
                      url?: string;
                  }

                  Properties

                  Properties

                  bin?: string

                  Specifies the data contained in the artifact

                  url?: string

                  MUST be a valid URL that resolves to the un-encoded content

                  -
                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Process.html b/docs/interfaces/_mdf.js_openc2-core.Control.Process.html similarity index 57% rename from docs/interfaces/_mdf_js_openc2_core.Control.Process.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Process.html index 4b12de94..f247b7d6 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Process.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Process.html @@ -1,14 +1,14 @@ -Process | @mdf.js

                  A "Process" Target MUST contain at least one property

                  -
                  interface Process {
                      command_line?: string;
                      cwd?: string;
                      executable?: File;
                      name?: string;
                      parent?: Process;
                      pid?: number;
                  }

                  Properties

                  command_line?: string

                  The full command line invocation used to start this process, including all arguments

                  +Process | @mdf.js

                  A "Process" Target MUST contain at least one property

                  +
                  interface Process {
                      command_line?: string;
                      cwd?: string;
                      executable?: File;
                      name?: string;
                      parent?: Process;
                      pid?: number;
                  }

                  Properties

                  command_line?: string

                  The full command line invocation used to start this process, including all arguments

                  cwd?: string

                  Current working directory of the process

                  -
                  executable?: File

                  Executable that was executed to start the process

                  +
                  executable?: File

                  Executable that was executed to start the process

                  name?: string

                  Name of the process

                  -
                  parent?: Process

                  Process that spawned this one

                  +
                  parent?: Process

                  Process that spawned this one

                  pid?: number

                  Process ID of the process

                  -
                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Response.html b/docs/interfaces/_mdf.js_openc2-core.Control.Response.html similarity index 52% rename from docs/interfaces/_mdf_js_openc2_core.Control.Response.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Response.html index 72f9e700..d5ab089a 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Response.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Response.html @@ -1,8 +1,8 @@ -Response | @mdf.js

                  Response object

                  -
                  interface Response {
                      results?: Results;
                      status: StatusCode;
                      status_text?: string;
                  }

                  Properties

                  results?: Results

                  Map of key:value pairs that contain additional results based on the invoking Command

                  -
                  status: StatusCode

                  An integer status code

                  -
                  status_text?: string

                  A free-form human-readable description of the Response status

                  -
                  +Response | @mdf.js

                  Response object

                  +
                  interface Response {
                      results?: Results;
                      status: StatusCode;
                      status_text?: string;
                  }

                  Properties

                  results?: Results

                  Map of key:value pairs that contain additional results based on the invoking Command

                  +
                  status: StatusCode

                  An integer status code

                  +
                  status_text?: string

                  A free-form human-readable description of the Response status

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.Control.ResponseMessage.html b/docs/interfaces/_mdf.js_openc2-core.Control.ResponseMessage.html new file mode 100644 index 00000000..b7b5890c --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.Control.ResponseMessage.html @@ -0,0 +1,21 @@ +ResponseMessage | @mdf.js
                  interface ResponseMessage {
                      content: Control.Response;
                      content_type: string;
                      created: number;
                      from: string;
                      msg_type: Response;
                      request_id: string;
                      status: StatusCode;
                      to: string[];
                  }

                  Hierarchy (View Summary)

                  Properties

                  content: Control.Response

                  Message body as specified by content_type and msg_type

                  +
                  content_type: string

                  Media Type that identifies the format of the content, including major version. Incompatible +content formats must have different content_types. Content_type application/openc2 identifies +content defined by OpenC2 language specification versions 1.x, i.e., all versions that are +compatible with version 1.0.

                  +
                  created: number

                  Creation date/time of the content

                  +
                  from: string

                  Authenticated identifier of the creator of or authority for execution of a message

                  +
                  msg_type: Response

                  The type of command and control Message

                  +
                  request_id: string

                  A unique identifier created by the Producer and copied by Consumer into all Responses, in order +to support reference to a particular Command, transaction, or event chain

                  +
                  status: StatusCode

                  Populated with a numeric status code in Response

                  +
                  to: string[]

                  Authenticated identifier(s) of the authorized recipient(s) of a message

                  +
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Results.html b/docs/interfaces/_mdf.js_openc2-core.Control.Results.html similarity index 55% rename from docs/interfaces/_mdf_js_openc2_core.Control.Results.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Results.html index c8781fcf..620d0485 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Results.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Results.html @@ -1,10 +1,10 @@ -Results | @mdf.js

                  Result object

                  -
                  interface Results {
                      pairs?: ActionTargetPairs;
                      profiles?: string[];
                      rate_limit?: number;
                      versions?: string[];
                      [namespace: Namespace]: AllowedResultPropertyTypes;
                  }

                  Indexable

                  Properties

                  Map of each action supported by this actuator to the list of targets applicable to that action

                  +Results | @mdf.js

                  Result object

                  +
                  interface Results {
                      pairs?: ActionTargetPairs;
                      profiles?: string[];
                      rate_limit?: number;
                      versions?: string[];
                      [namespace: `x-${string}`]: AllowedResultPropertyTypes;
                  }

                  Indexable

                  Properties

                  Map of each action supported by this actuator to the list of targets applicable to that action

                  profiles?: string[]

                  List of profiles supported by this Actuator

                  -
                  rate_limit?: number

                  Maximum number of requests per minute supported by design or policy

                  -
                  versions?: string[]
                  +
                  rate_limit?: number

                  Maximum number of requests per minute supported by design or policy

                  +
                  versions?: string[]
                  diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Target.html b/docs/interfaces/_mdf.js_openc2-core.Control.Target.html similarity index 60% rename from docs/interfaces/_mdf_js_openc2_core.Control.Target.html rename to docs/interfaces/_mdf.js_openc2-core.Control.Target.html index bb3df843..5e3bbb3b 100644 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Target.html +++ b/docs/interfaces/_mdf.js_openc2-core.Control.Target.html @@ -1,29 +1,29 @@ -Target | @mdf.js

                  The target field in a Command MUST contain exactly one type of Target (e.g., ipv4_net)

                  -
                  interface Target {
                      artifact?: Artifact;
                      command?: string;
                      device?: Device;
                      domain_name?: string;
                      email_addr?: string;
                      features?: Features[];
                      file?: File;
                      idn_domain_name?: string;
                      idn_email_addr?: string;
                      ipv4_connection?: IPv4Connection;
                      ipv4_net?: string;
                      ipv6_connection?: IPv6Connection;
                      ipv6_net?: string;
                      iri?: string;
                      mac_addr?: string;
                      process?: Process;
                      properties?: string[];
                      uri?: string;
                      [namespace: Namespace]: any;
                  }

                  Indexable

                  • [namespace: Namespace]: any

                    Custom target formats

                    -

                  Properties

                  artifact?: Artifact

                  An array of bytes representing a file-like object or a link to that object

                  +Target | @mdf.js

                  The target field in a Command MUST contain exactly one type of Target (e.g., ipv4_net)

                  +
                  interface Target {
                      artifact?: Artifact;
                      command?: string;
                      device?: Device;
                      domain_name?: string;
                      email_addr?: string;
                      features?: Features[];
                      file?: File;
                      idn_domain_name?: string;
                      idn_email_addr?: string;
                      ipv4_connection?: IPv4Connection;
                      ipv4_net?: string;
                      ipv6_connection?: IPv6Connection;
                      ipv6_net?: string;
                      iri?: string;
                      mac_addr?: string;
                      process?: Process;
                      properties?: string[];
                      uri?: string;
                      [namespace: `x-${string}`]: any;
                  }

                  Indexable

                  • [namespace: `x-${string}`]: any

                    Custom target formats

                    +

                  Properties

                  artifact?: Artifact

                  An array of bytes representing a file-like object or a link to that object

                  command?: string

                  A reference to a previously issued Command

                  -
                  device?: Device

                  The properties of a hardware device

                  -
                  domain_name?: string

                  A network domain name

                  -
                  email_addr?: string

                  A single email address

                  -
                  features?: Features[]

                  A set of items used with the query Action to determine an Actuator's capabilities +

                  device?: Device

                  The properties of a hardware device

                  +
                  domain_name?: string

                  A network domain name

                  +
                  email_addr?: string

                  A single email address

                  +
                  features?: Features[]

                  A set of items used with the query Action to determine an Actuator's capabilities An array of zero to ten names used to query an Actuator for its supported capabilities A Producer MUST NOT send a list containing more than one instance of any Feature. A Consumer receiving a list containing more than one instance of any Feature SHOULD behave as @@ -33,17 +33,17 @@ idle traffic to keep a connection to a Consumer from being closed due to inactivity (a keep-alive command). An active Consumer could return an empty response to this command, minimizing the amount of traffic used to perform heartbeat / keep-alive functions.

                  -
                  file?: File

                  Properties of a file

                  -
                  idn_domain_name?: string

                  An internationalized domain name

                  -
                  idn_email_addr?: string

                  A single internationalized email address

                  -
                  ipv4_connection?: IPv4Connection

                  An IPv4 address range including CIDR prefix length

                  -
                  ipv4_net?: string

                  An IPv6 address range including prefix length

                  -
                  ipv6_connection?: IPv6Connection

                  A 5-tuple of source and destination IPv4 address ranges, source and destination ports, and +

                  file?: File

                  Properties of a file

                  +
                  idn_domain_name?: string

                  An internationalized domain name

                  +
                  idn_email_addr?: string

                  A single internationalized email address

                  +
                  ipv4_connection?: IPv4Connection

                  An IPv4 address range including CIDR prefix length

                  +
                  ipv4_net?: string

                  An IPv6 address range including prefix length

                  +
                  ipv6_connection?: IPv6Connection

                  A 5-tuple of source and destination IPv4 address ranges, source and destination ports, and protocol

                  -
                  ipv6_net?: string

                  An IPv6 address range including prefix length

                  +
                  ipv6_net?: string

                  An IPv6 address range including prefix length

                  iri?: string

                  An internationalized resource identifier (IRI)

                  -
                  mac_addr?: string

                  A Media Access Control (MAC) address - EUI-48 or EUI-64 as defined in [EUI]

                  -
                  process?: Process

                  Common properties of an instance of a computer program as executed on an operating system

                  +
                  mac_addr?: string

                  A Media Access Control (MAC) address - EUI-48 or EUI-64 as defined in [EUI]

                  +
                  process?: Process

                  Common properties of an instance of a computer program as executed on an operating system

                  properties?: string[]

                  Data attribute associated with an Actuator

                  uri?: string

                  A uniform resource identifier (URI)

                  -
                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.GatewayOptions.html b/docs/interfaces/_mdf.js_openc2-core.GatewayOptions.html new file mode 100644 index 00000000..6bfea2ad --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.GatewayOptions.html @@ -0,0 +1,32 @@ +GatewayOptions | @mdf.js
                  interface GatewayOptions {
                      actionTargetPairs: ActionTargetPairs;
                      actuator?: string[];
                      agingInterval?: number;
                      bypassLookupIntervalChecks?: boolean;
                      delay?: number;
                      id: string;
                      logger?: LoggerInstance;
                      lookupInterval?: number;
                      lookupTimeout?: number;
                      maxAge?: number;
                      maxInactivityTime?: number;
                      profiles?: string[];
                      registerLimit?: number;
                      registry?: Registry;
                      resolver?: ResolverMap;
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  actionTargetPairs: ActionTargetPairs

                  Supported pairs Action-Target pairs

                  +
                  actuator?: string[]

                  Actuator

                  +
                  agingInterval?: number
                  bypassLookupIntervalChecks?: boolean

                  Bypass lookup interval times checks

                  +
                  delay?: number

                  Gateway delay

                  +
                  id: string

                  Instance identification

                  +

                  Logger instance

                  +
                  lookupInterval?: number

                  Lookup interval in milliseconds

                  +
                  lookupTimeout?: number

                  Lookup timeout in milliseconds

                  +
                  maxAge?: number

                  Max allowed age in in milliseconds for a table entry

                  +
                  maxInactivityTime?: number

                  Register max inactivity time

                  +
                  profiles?: string[]

                  Supported profiles

                  +
                  registerLimit?: number

                  Maximum number of message to be stored

                  +
                  registry?: Registry

                  Message and jobs registry

                  +
                  resolver?: ResolverMap

                  Resolver

                  +
                  retryOptions?: RetryOptions

                  Options for adapter retry operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core.ProducerAdapter.html b/docs/interfaces/_mdf.js_openc2-core.ProducerAdapter.html new file mode 100644 index 00000000..bb280b54 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.ProducerAdapter.html @@ -0,0 +1,17 @@ +ProducerAdapter | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface ProducerAdapter {
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      publish(
                          message: CommandMessage,
                      ): Promise<void | ResponseMessage | ResponseMessage[]>;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Hierarchy (View Summary)

                  Implemented by

                  Methods

                  Methods

                  • Emitted when a adapter's operation has some error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every state change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_openc2-core.ProducerOptions.html b/docs/interfaces/_mdf.js_openc2-core.ProducerOptions.html new file mode 100644 index 00000000..752f8c2d --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core.ProducerOptions.html @@ -0,0 +1,20 @@ +ProducerOptions | @mdf.js
                  interface ProducerOptions {
                      agingInterval?: number;
                      id: string;
                      logger?: LoggerInstance;
                      lookupInterval?: number;
                      lookupTimeout?: number;
                      maxAge?: number;
                      maxInactivityTime?: number;
                      registerLimit?: number;
                      registry?: Registry;
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  agingInterval?: number
                  id: string

                  Instance identification

                  +

                  Logger instance

                  +
                  lookupInterval?: number

                  Lookup interval in milliseconds

                  +
                  lookupTimeout?: number

                  Lookup timeout in milliseconds

                  +
                  maxAge?: number

                  Max allowed age in in milliseconds for a table entry

                  +
                  maxInactivityTime?: number

                  Register max inactivity time

                  +
                  registerLimit?: number

                  Maximum number of message to be stored

                  +
                  registry?: Registry

                  Message and jobs registry

                  +
                  retryOptions?: RetryOptions

                  Options for adapter retry operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core._internal_.BaseMessage.html b/docs/interfaces/_mdf.js_openc2-core._internal_.BaseMessage.html new file mode 100644 index 00000000..cb434977 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core._internal_.BaseMessage.html @@ -0,0 +1,15 @@ +BaseMessage | @mdf.js
                  interface BaseMessage {
                      content_type: string;
                      created: number;
                      from: string;
                      request_id: string;
                      to: string[];
                  }

                  Hierarchy (View Summary)

                  Properties

                  content_type: string

                  Media Type that identifies the format of the content, including major version. Incompatible +content formats must have different content_types. Content_type application/openc2 identifies +content defined by OpenC2 language specification versions 1.x, i.e., all versions that are +compatible with version 1.0.

                  +
                  created: number

                  Creation date/time of the content

                  +
                  from: string

                  Authenticated identifier of the creator of or authority for execution of a message

                  +
                  request_id: string

                  A unique identifier created by the Producer and copied by Consumer into all Responses, in order +to support reference to a particular Command, transaction, or event chain

                  +
                  to: string[]

                  Authenticated identifier(s) of the authorized recipient(s) of a message

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentAdapter.html b/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentAdapter.html new file mode 100644 index 00000000..97dc26c6 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentAdapter.html @@ -0,0 +1,14 @@ +ComponentAdapter | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, +besides the error handling and identity, it has a start, stop and close methods to manage the +resource lifecycle. It also has a checks property to define the checks that will be performed +over the resource to achieve the resulted status. +The most typical example of a resource are the Provider that allow to access to external +databases, message brokers, etc.

                  +
                  interface ComponentAdapter {
                      on(event: "error", listener: (error: Crash | Error) => void): this;
                      on(
                          event: "status",
                          listener: (status: "pass" | "fail" | "warn") => void,
                      ): this;
                      start(): Promise<void>;
                      stop(): Promise<void>;
                  }

                  Hierarchy (View Summary)

                  Methods

                  Methods

                  • Emitted when a adapter's operation has some error

                    +

                    Parameters

                    • event: "error"
                    • listener: (error: Crash | Error) => void

                    Returns this

                  • Emitted on every state change

                    +

                    Parameters

                    • event: "status"
                    • listener: (status: "pass" | "fail" | "warn") => void

                    Returns this

                  • Connect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  • Disconnect the OpenC2 Adapter to the underlayer transport system

                    +

                    Returns Promise<void>

                  diff --git a/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentOptions.html b/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentOptions.html new file mode 100644 index 00000000..2407e349 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core._internal_.ComponentOptions.html @@ -0,0 +1,13 @@ +ComponentOptions | @mdf.js
                  interface ComponentOptions {
                      id: string;
                      logger?: LoggerInstance;
                      maxInactivityTime?: number;
                      registerLimit?: number;
                      registry?: Registry;
                      retryOptions?: RetryOptions;
                  }

                  Hierarchy (View Summary)

                  Properties

                  id: string

                  Instance identification

                  +

                  Logger instance

                  +
                  maxInactivityTime?: number

                  Register max inactivity time

                  +
                  registerLimit?: number

                  Maximum number of message to be stored

                  +
                  registry?: Registry

                  Message and jobs registry

                  +
                  retryOptions?: RetryOptions

                  Options for adapter retry operations

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2-core._internal_.GatewayTimers.html b/docs/interfaces/_mdf.js_openc2-core._internal_.GatewayTimers.html new file mode 100644 index 00000000..82690a09 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2-core._internal_.GatewayTimers.html @@ -0,0 +1,6 @@ +GatewayTimers | @mdf.js
                  interface GatewayTimers {
                      agingInterval: number;
                      delay: number;
                      lookupInterval: number;
                      lookupTimeout: number;
                      maxAge: number;
                  }

                  Properties

                  agingInterval: number
                  delay: number
                  lookupInterval: number
                  lookupTimeout: number
                  maxAge: number
                  diff --git a/docs/interfaces/_mdf.js_openc2.AdapterOptions.html b/docs/interfaces/_mdf.js_openc2.AdapterOptions.html new file mode 100644 index 00000000..25a46059 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2.AdapterOptions.html @@ -0,0 +1,10 @@ +AdapterOptions | @mdf.js

                  Interface AdapterOptions

                  Adapter options

                  +
                  interface AdapterOptions {
                      actuators?: string[];
                      id: string;
                      separator?: string;
                      token?: string;
                  }

                  Properties

                  actuators?: string[]

                  Actuators

                  +
                  id: string

                  Instance identification

                  +
                  separator?: string

                  Channel scope separator

                  +
                  token?: string

                  Authorization token

                  +
                  diff --git a/docs/interfaces/_mdf.js_openc2.ServiceBusOptions.html b/docs/interfaces/_mdf.js_openc2.ServiceBusOptions.html new file mode 100644 index 00000000..a04e6f24 --- /dev/null +++ b/docs/interfaces/_mdf.js_openc2.ServiceBusOptions.html @@ -0,0 +1,5 @@ +ServiceBusOptions | @mdf.js

                  Interface ServiceBusOptions

                  interface ServiceBusOptions {
                      secret?: string;
                      useJwt?: boolean;
                  }

                  Properties

                  Properties

                  secret?: string

                  Secret used in JWT token validation

                  +
                  useJwt?: boolean

                  Define the use of JWT tokens for client authentication

                  +
                  diff --git a/docs/interfaces/_mdf.js_redis-provider._internal_.MemoryStats.html b/docs/interfaces/_mdf.js_redis-provider._internal_.MemoryStats.html new file mode 100644 index 00000000..88b3d621 --- /dev/null +++ b/docs/interfaces/_mdf.js_redis-provider._internal_.MemoryStats.html @@ -0,0 +1,105 @@ +MemoryStats | @mdf.js
                  interface MemoryStats {
                      active_defrag_running: string;
                      allocator_active: string;
                      allocator_allocated: string;
                      allocator_frag_bytes: string;
                      allocator_frag_ratio: string;
                      allocator_resident: string;
                      allocator_rss_bytes: string;
                      allocator_rss_ratio: string;
                      errorParsing?: string;
                      lazyfree_pending_objects: string;
                      lazyfreed_objects: string;
                      maxmemory: string;
                      maxmemory_human: string;
                      maxmemory_policy: string;
                      mem_allocator: string;
                      mem_aof_buffer: string;
                      mem_clients_normal: string;
                      mem_clients_slaves: string;
                      mem_cluster_links: string;
                      mem_fragmentation_bytes: string;
                      mem_fragmentation_ratio: string;
                      mem_not_counted_for_evict: string;
                      mem_replication_backlog: string;
                      mem_total_replication_buffers: string;
                      rss_overhead_bytes: string;
                      rss_overhead_ratio: string;
                      total_system_memory: string;
                      total_system_memory_human: string;
                      used_memory: string;
                      used_memory_dataset: string;
                      used_memory_dataset_perc: string;
                      used_memory_human: string;
                      used_memory_lua: string;
                      used_memory_lua_human: string;
                      used_memory_overhead: string;
                      used_memory_peak: string;
                      used_memory_peak_human: string;
                      used_memory_peak_perc: string;
                      used_memory_rss: string;
                      used_memory_rss_human: string;
                      used_memory_scripts: string;
                      used_memory_scripts_human: string;
                      used_memory_startup: string;
                  }

                  Properties

                  active_defrag_running: string

                  When activedefrag is enabled, this indicates whether defragmentation is currently active, and +the CPU percentage it intends to utilize

                  +
                  allocator_active: string

                  Total bytes in the allocator active pages, this includes external-fragmentation.

                  +
                  allocator_allocated: string

                  Total bytes allocated form the allocator, including internal-fragmentation. Normally the same +as used_memory

                  +
                  allocator_frag_bytes: string

                  Delta between allocator_active and allocator_allocated. See note about +mem_fragmentation_bytes.

                  +
                  allocator_frag_ratio: string

                  Ratio between allocator_active and allocator_allocated. This is the true (external) +fragmentation metric (not mem_fragmentation_ratio).

                  +
                  allocator_resident: string

                  Total bytes resident (RSS) in the allocator, this includes pages that can be released to the +OS (by MEMORY PURGE, or just waiting)

                  +
                  allocator_rss_bytes: string

                  Delta between allocator_resident and allocator_active

                  +
                  allocator_rss_ratio: string

                  Ratio between allocator_resident and allocator_active. This usually indicates pages that the +allocator can and probably will soon release back to the OS

                  +
                  errorParsing?: string

                  Error parsing message

                  +
                  lazyfree_pending_objects: string

                  The number of objects waiting to be freed (as a result of calling UNLINK, or FLUSHDB and +FLUSHALL with the ASYNC option)

                  +
                  lazyfreed_objects: string

                  The number of objects that have been lazy freed.

                  +
                  maxmemory: string

                  The value of the maxmemory configuration directive

                  +
                  maxmemory_human: string

                  Human readable representation of previous value

                  +
                  maxmemory_policy: string

                  The value of the maxmemory-policy configuration directive

                  +
                  mem_allocator: string

                  Memory allocator, chosen at compile time.

                  +
                  mem_aof_buffer: string

                  Transient memory used for AOF and AOF rewrite buffers

                  +
                  mem_clients_normal: string

                  Memory used by normal clients

                  +
                  mem_clients_slaves: string

                  Memory used by replica clients - Starting Redis 7.0, replica buffers share memory with the +replication backlog, so this field can show 0 when replicas don't trigger an increase of +memory usage

                  +
                  mem_cluster_links: string

                  Memory used by links to peers on the cluster bus when cluster mode is enabled.

                  +
                  mem_fragmentation_bytes: string

                  Delta between used_memory_rss and used_memory. Note that when the total fragmentation bytes +is low (few megabytes), a high ratio (e.g. 1.5 and above) is not an indication of an issue

                  +
                  mem_fragmentation_ratio: string

                  Ratio between used_memory_rss and used_memory. Note that this doesn't only includes +fragmentation, but also other process overheads (see the allocator_* metrics), and also +overheads like code, shared libraries, stack, etc.

                  +
                  mem_not_counted_for_evict: string

                  Used memory that's not counted for key eviction. This is basically transient replica and AOF +buffers

                  +
                  mem_replication_backlog: string

                  Memory used by replication backlog

                  +
                  mem_total_replication_buffers: string

                  Total memory consumed for replication buffers - Added in Redis 7.0.

                  +
                  rss_overhead_bytes: string

                  Delta between used_memory_rss (the process RSS) and allocator_resident

                  +
                  rss_overhead_ratio: string

                  Ratio between used_memory_rss (the process RSS) and allocator_resident. This includes RSS +overheads that are not allocator or heap related

                  +
                  total_system_memory: string

                  The total amount of memory that the Redis host has

                  +
                  total_system_memory_human: string

                  Human readable representation of previous value

                  +
                  used_memory: string

                  Total number of bytes allocated by Redis using its allocator (either standard libc, jemalloc, +or an alternative allocator such as tcmalloc)

                  +
                  used_memory_dataset: string

                  The size in bytes of the dataset (used_memory_overhead subtracted from used_memory)

                  +
                  used_memory_dataset_perc: string

                  The percentage of used_memory_dataset out of the net memory usage (used_memory minus +used_memory_startup)

                  +
                  used_memory_human: string

                  Human readable representation of previous value

                  +
                  used_memory_lua: string

                  Number of bytes used by the Lua engine

                  +
                  used_memory_lua_human: string

                  Human readable representation of previous value

                  +
                  used_memory_overhead: string

                  The sum in bytes of all overheads that the server allocated for managing its internal data +structures

                  +
                  used_memory_peak: string

                  Peak memory consumed by Redis (in bytes)

                  +
                  used_memory_peak_human: string

                  Human readable representation of previous value

                  +
                  used_memory_peak_perc: string

                  The percentage of used_memory_peak out of used_memory

                  +
                  used_memory_rss: string

                  Number of bytes that Redis allocated as seen by the operating system (a.k.a resident set +size). This is the number reported by tools such as top(1) and ps(1)

                  +
                  used_memory_rss_human: string

                  Human readable representation of previous value

                  +
                  used_memory_scripts: string

                  Number of bytes used by cached Lua scripts

                  +
                  used_memory_scripts_human: string

                  Human readable representation of previous value

                  +
                  used_memory_startup: string

                  Initial amount of memory consumed by Redis at startup in bytes

                  +
                  diff --git a/docs/interfaces/_mdf.js_redis-provider._internal_.ServerStats.html b/docs/interfaces/_mdf.js_redis-provider._internal_.ServerStats.html new file mode 100644 index 00000000..ea86323b --- /dev/null +++ b/docs/interfaces/_mdf.js_redis-provider._internal_.ServerStats.html @@ -0,0 +1,52 @@ +ServerStats | @mdf.js
                  interface ServerStats {
                      arch_bits: number;
                      atomicvar_api: string;
                      config_file: string;
                      configured_hz: string;
                      errorParsing?: string;
                      executable: string;
                      gcc_version: string;
                      hz: string;
                      io_threads_active: string;
                      lru_clock: string;
                      multiplexing_api: string;
                      os: string;
                      process_id: string;
                      process_supervised: ProcessesSupervised;
                      redis_build_id: string;
                      redis_git_dirty: string;
                      redis_git_sha1: string;
                      redis_mode: "standalone" | "sentinel" | "cluster";
                      redis_version: string;
                      run_id: string;
                      server_time_usec: string;
                      shutdown_in_milliseconds: string;
                      tcp_port: string;
                      uptime_in_days: string;
                      uptime_in_seconds: string;
                  }

                  Properties

                  arch_bits: number

                  Architecture (32 or 64 bits)

                  +
                  atomicvar_api: string

                  Atomicvar API used by Redis

                  +
                  config_file: string

                  The path to the config file

                  +
                  configured_hz: string

                  The server's configured frequency setting

                  +
                  errorParsing?: string

                  Error parsing message

                  +
                  executable: string

                  The path to the server's executable

                  +
                  gcc_version: string

                  Version of the GCC compiler used to compile the Redis server

                  +
                  hz: string

                  The server's current frequency setting

                  +
                  io_threads_active: string

                  Flag indicating if I/O threads are active

                  +
                  lru_clock: string

                  Clock incrementing every minute, for LRU management

                  +
                  multiplexing_api: string

                  Event loop mechanism used by Redis

                  +
                  os: string

                  Operating system hosting the Redis server

                  +
                  process_id: string

                  PID of the server process

                  +
                  process_supervised: ProcessesSupervised

                  Supervised system ("upstart", "systemd", "unknown" or "no")

                  +
                  redis_build_id: string

                  The build id

                  +
                  redis_git_dirty: string

                  Git dirty flag

                  +
                  redis_git_sha1: string

                  Git SHA1

                  +
                  redis_mode: "standalone" | "sentinel" | "cluster"

                  The server's mode ("standalone", "sentinel" or "cluster")

                  +
                  redis_version: string

                  Version of the Redis server

                  +
                  run_id: string

                  Random value identifying the Redis server (to be used by Sentinel and Cluster)

                  +
                  server_time_usec: string

                  Epoch-based system time with microsecond precision

                  +
                  shutdown_in_milliseconds: string

                  The maximum time remaining for replicas to catch up the replication before completing the +shutdown sequence. This field is only present during shutdown

                  +
                  tcp_port: string

                  TCP/IP listen port

                  +
                  uptime_in_days: string

                  Same value expressed in days

                  +
                  uptime_in_seconds: string

                  Number of seconds since Redis server start

                  +
                  diff --git a/docs/interfaces/_mdf.js_redis-provider._internal_.Status.html b/docs/interfaces/_mdf.js_redis-provider._internal_.Status.html new file mode 100644 index 00000000..479c06cf --- /dev/null +++ b/docs/interfaces/_mdf.js_redis-provider._internal_.Status.html @@ -0,0 +1,5 @@ +Status | @mdf.js
                  interface Status {
                      memory: MemoryStats;
                      server: ServerStats;
                  }

                  Properties

                  Properties

                  memory: MemoryStats

                  Redis memory information section

                  +
                  server: ServerStats

                  Redis server information section

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry.BootstrapOptions.html b/docs/interfaces/_mdf.js_service-registry.BootstrapOptions.html new file mode 100644 index 00000000..cb52b9d6 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.BootstrapOptions.html @@ -0,0 +1,74 @@ +BootstrapOptions | @mdf.js

                  Bootstrap options

                  +
                  interface BootstrapOptions {
                      configFiles?: string[];
                      consumer?: boolean;
                      loadPackage?: boolean;
                      loadReadme?: string | boolean;
                      preset?: string;
                      presetFiles?: string[];
                      useEnvironment?: boolean;
                  }

                  Properties

                  configFiles?: string[]

                  List of files with deploying options to be loaded. The entries could be a file path or +glob pattern. It supports configurations in JSON, YAML, TOML, and .env file formats.

                  +
                  `['./config/*.json']`
                  +
                  + +
                  `['./config/logger.json', './config/metadata.yaml']`
                  +
                  + +
                  consumer?: boolean

                  Flag indicating if the OpenC2 Consumer command interface should be enabled. The command +interface is a set of commands that can be used to interact with the application. +The commands are exposed in the observability endpoints and can be used to interact with the +service, or, if a consumer adapter is configured, to interact with the service from a central +controller.

                  +
                  loadPackage?: boolean

                  Flag indicating that the package.json file should be loaded. If this flag is set to true, the +the module will scale parent directories looking for a package.json file to load, if the file +is found, the package information will be used to fullfil the metadata field.

                  +
                    +
                  • package.name will be used as the metadata.name.
                  • +
                  • package.version will be used as the metadata.version, and the first part of the version +will be used as the metadata.release.
                  • +
                  • package.description will be used as the metadata.description.
                  • +
                  • package.keywords will be used as the metadata.tags.
                  • +
                  • package.config.${name}, where name is the name of the configuration, will be used to find +the rest of properties with the same name that in the metadata. +This information will be merged with the rest of the configuration, overriding the +configuration from files, but not the configuration passed as argument to Service Registry.
                  • +
                  +
                  loadReadme?: string | boolean

                  Flag indicating that the README.md file should be loaded. If this flag is set to true, the +module will scale parent directories looking for a README.md file to load, if the file is +found, the README content will be exposed in the observability endpoints. +If the flag is a string, the string will be used as the file name to look for.

                  +
                  preset?: string

                  Preset to be used as configuration base, if none is indicated, or the indicated preset is +not found, the configuration from the configuration files will be used.

                  +
                  presetFiles?: string[]

                  List of files with preset options to be loaded. The entries could be a file path or glob +pattern. The first part of the file name will be used as the preset name. The file name +should be in the format of presetName.config.json or presetName.config.yaml. The name of +the preset will be used to merge different files in order to create a single preset.

                  +
                  `['./config/presets/*.json']`
                  +
                  + +
                  `['./config/presets/*.json', './config/presets/*.yaml']`
                  +
                  + +
                  `['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml']`
                  +
                  + +
                  useEnvironment?: boolean

                  Flag indicating that the environment configuration variables should be used. The configuration +loaded by environment variables will be merged with the rest of the configuration, overriding +the configuration from files, but not the configuration passed as argument to Service Registry. +When option is set some filters are applied to the environment variables to avoid conflicts in +the configuration. The filters are:

                  +
                    +
                  • CONFIG_METADATA_: Application metadata configuration.
                  • +
                  • CONFIG_OBSERVABILITY_: Observability service configuration.
                  • +
                  • CONFIG_LOGGER_: Logger configuration.
                  • +
                  • CONFIG_RETRY_OPTIONS_: Retry options configuration.
                  • +
                  • CONFIG_ADAPTER_: Consumer adapter configuration.
                  • +
                  +

                  The loader expect environment configuration variables represented in SCREAMING_SNAKE_CASE, +that will parsed to camelCase and merged with the rest of the configuration. The consumer +adapter configuration is an exception, due to the kind of configuration, it should be provided +by configuration parameters.

                  +
                  CONFIG_METADATA_NAME=MyApp
                  CONFIG_METADATA_LINKS__SELF=https://myapp.com
                  CONFIG_OBSERVABILITY_PORT=8080
                  CONFIG_LOGGER__CONSOLE__LEVEL=info
                  CONFIG_RETRY_OPTIONS_ATTEMPTS=3
                  CONFIG_ADAPTER_TYPE=redis +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ExtendedCrashObject.html b/docs/interfaces/_mdf.js_service-registry.ExtendedCrashObject.html new file mode 100644 index 00000000..da2f4e21 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ExtendedCrashObject.html @@ -0,0 +1,5 @@ +ExtendedCrashObject | @mdf.js

                  Extended Crash object including workerId, workerPid and stack

                  +
                  interface ExtendedCrashObject {
                      stack?: string;
                      workerId?: number;
                      workerPid?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  stack?: string
                  workerId?: number
                  workerPid?: number
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ExtendedMultiObject.html b/docs/interfaces/_mdf.js_service-registry.ExtendedMultiObject.html new file mode 100644 index 00000000..3c637c26 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ExtendedMultiObject.html @@ -0,0 +1,5 @@ +ExtendedMultiObject | @mdf.js

                  Extended Multi object including workerId, workerPid and stack

                  +
                  interface ExtendedMultiObject {
                      stack?: string;
                      workerId?: number;
                      workerPid?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  stack?: string
                  workerId?: number
                  workerPid?: number
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ObservabilityServiceOptions.html b/docs/interfaces/_mdf.js_service-registry.ObservabilityServiceOptions.html new file mode 100644 index 00000000..1f1c171d --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ObservabilityServiceOptions.html @@ -0,0 +1,18 @@ +ObservabilityServiceOptions | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface ObservabilityServiceOptions {
                      clusterUpdateInterval?: number;
                      host?: string;
                      includeStack?: boolean;
                      isCluster?: boolean;
                      maxSize?: number;
                      port?: number;
                      primaryPort?: number;
                  }

                  Properties

                  clusterUpdateInterval?: number

                  Cluster polling interval

                  +
                  host?: string

                  Host IP Addresses to be attached

                  +
                  includeStack?: boolean

                  Include stack trace in the error

                  +
                  isCluster?: boolean

                  Enable cluster mode

                  +
                  maxSize?: number

                  Max size of the registry

                  +
                  port?: number

                  Port to listen for incoming requests

                  +
                  primaryPort?: number

                  Primary port to listen for incoming requests on cluster mode

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ServiceRegistryOptions.html b/docs/interfaces/_mdf.js_service-registry.ServiceRegistryOptions.html new file mode 100644 index 00000000..81d4ee02 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ServiceRegistryOptions.html @@ -0,0 +1,43 @@ +ServiceRegistryOptions | @mdf.js

                  Interface ServiceRegistryOptions<CustomSettings>

                  Deploying configuration options. This configuration is used to setup the application or +microservice to be deployed in a specific environment, we can configure:

                  +
                    +
                  • Application/microservice metadata information.
                  • +
                  • Remoto control interface (OpenC2 Consumer)
                  • +
                  • Observability service.
                  • +
                  • Logger configuration.
                  • +
                  • Retry options.
                  • +
                  +
                  interface ServiceRegistryOptions<
                      CustomSettings extends
                          Record<string, CustomSetting> = Record<string, CustomSetting>,
                  > {
                      adapterOptions?: ConsumerAdapterOptions;
                      configLoaderOptions?: Partial<Setup.Config<CustomSettings>>;
                      consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>;
                      loggerOptions?: Partial<LoggerConfig>;
                      metadata?: Partial<Metadata>;
                      observabilityOptions?: Partial<ObservabilityServiceOptions>;
                      retryOptions?: Partial<
                          Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">,
                      >;
                  }

                  Type Parameters

                  Hierarchy (View Summary)

                  Properties

                  adapterOptions?: ConsumerAdapterOptions

                  Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, +consumer and adapter options must be provided, in other case the consumer will start with +a Dummy adapter with no connection to any external service, so only HTTP commands over the +observability endpoints will be processed.

                  +
                  configLoaderOptions?: Partial<Setup.Config<CustomSettings>>

                  Configuration loader options. These options is used to load the configuration information +of the application that is been wrapped by the Application Wrapper. This configuration could be +loaded from files or environment variables, or even both.

                  +

                  To understand the configuration loader options, check the documentation of the package +@mdf.js/service-setup-provider.

                  +

                  Use different files for Application Wrapper configuration and for your own services to +avoid conflicts.

                  +
                  consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>

                  OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 +consumer, that is used to receive and process OpenC2 commands. The consumer option in the +BootstrapOptions should be enabled to start the consumer.

                  +
                  loggerOptions?: Partial<LoggerConfig>

                  Logger Options. If provided, a logger instance from the @mdf.js/logger package will be +created and used by the application in all the internal services of the Application Wrapper. +At the same time, the logger is exposed to the application to be used in the application +services. If this options is not provided, a Debug logger will be used internally, but it +will not be exposed to the application.

                  +
                  metadata?: Partial<Metadata>

                  Metadata information of the application or microservice. This information is used to identify +the application in the logs, metrics, and traces... and is shown in the service observability +endpoints.

                  +
                  observabilityOptions?: Partial<ObservabilityServiceOptions>

                  Observability instance options

                  +
                  retryOptions?: Partial<
                      Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">,
                  >

                  Retry options. If provided, the application will use this options to retry to start the +services/resources registered in the Application Wrapped instance. If this options is not +provided, the application will not retry to start the services/resources.

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ServiceRegistrySettings.html b/docs/interfaces/_mdf.js_service-registry.ServiceRegistrySettings.html new file mode 100644 index 00000000..b6c422cc --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ServiceRegistrySettings.html @@ -0,0 +1,43 @@ +ServiceRegistrySettings | @mdf.js

                  Interface ServiceRegistrySettings<CustomSettings>

                  Deploying configuration options. This configuration is used to setup the application or +microservice to be deployed in a specific environment, we can configure:

                  +
                    +
                  • Application/microservice metadata information.
                  • +
                  • Remoto control interface (OpenC2 Consumer)
                  • +
                  • Observability service.
                  • +
                  • Logger configuration.
                  • +
                  • Retry options.
                  • +
                  +
                  interface ServiceRegistrySettings<
                      CustomSettings extends
                          Record<string, CustomSetting> = Record<string, CustomSetting>,
                  > {
                      adapterOptions?: ConsumerAdapterOptions;
                      configLoaderOptions: Setup.Config<CustomSettings>;
                      consumerOptions?: ConsumerOptions;
                      loggerOptions: LoggerConfig;
                      metadata: Metadata;
                      observabilityOptions: ObservabilityServiceOptions;
                      retryOptions: RetryOptions;
                  }

                  Type Parameters

                  Hierarchy (View Summary)

                  Properties

                  adapterOptions?: ConsumerAdapterOptions

                  Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, +consumer and adapter options must be provided, in other case the consumer will start with +a Dummy adapter with no connection to any external service, so only HTTP commands over the +observability endpoints will be processed.

                  +
                  configLoaderOptions: Setup.Config<CustomSettings>

                  Configuration loader options. These options is used to load the configuration information +of the application that is been wrapped by the Application Wrapper. This configuration could be +loaded from files or environment variables, or even both.

                  +

                  To understand the configuration loader options, check the documentation of the package +@mdf.js/service-setup-provider.

                  +

                  Use different files for Application Wrapper configuration and for your own services to +avoid conflicts.

                  +
                  consumerOptions?: ConsumerOptions

                  OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 +consumer, that is used to receive and process OpenC2 commands. The consumer option in the +BootstrapOptions should be enabled to start the consumer.

                  +
                  loggerOptions: LoggerConfig

                  Logger Options. If provided, a logger instance from the @mdf.js/logger package will be +created and used by the application in all the internal services of the Application Wrapper. +At the same time, the logger is exposed to the application to be used in the application +services. If this options is not provided, a Debug logger will be used internally, but it +will not be exposed to the application.

                  +
                  metadata: Metadata

                  Metadata information of the application or microservice. This information is used to identify +the application in the logs, metrics, and traces... and is shown in the service observability +endpoints.

                  +
                  observabilityOptions: ObservabilityServiceOptions

                  Observability instance options

                  +
                  retryOptions: RetryOptions

                  Retry options. If provided, the application will use this options to retry to start the +services/resources registered in the Application Wrapped instance. If this options is not +provided, the application will not retry to start the services/resources.

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry.ServiceSetting.html b/docs/interfaces/_mdf.js_service-registry.ServiceSetting.html new file mode 100644 index 00000000..05c37595 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry.ServiceSetting.html @@ -0,0 +1,38 @@ +ServiceSetting | @mdf.js

                  Interface ServiceSetting<CustomSettings>

                  Service setting interface +Merge in the object the service registry settings and the custom settings.

                  +
                  interface ServiceSetting<
                      CustomSettings extends
                          Record<string, CustomSetting> = Record<string, CustomSetting>,
                  > {
                      adapterOptions?: ConsumerAdapterOptions;
                      configLoaderOptions?: Partial<Setup.Config<Record<string, any>>>;
                      consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>;
                      custom: CustomSettings;
                      loggerOptions?: Partial<LoggerConfig>;
                      metadata?: Partial<Metadata>;
                      observabilityOptions?: Partial<ObservabilityServiceOptions>;
                      retryOptions?: Partial<
                          Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">,
                      >;
                  }

                  Type Parameters

                  Hierarchy (View Summary)

                  Properties

                  adapterOptions?: ConsumerAdapterOptions

                  Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, +consumer and adapter options must be provided, in other case the consumer will start with +a Dummy adapter with no connection to any external service, so only HTTP commands over the +observability endpoints will be processed.

                  +
                  configLoaderOptions?: Partial<Setup.Config<Record<string, any>>>

                  Configuration loader options. These options is used to load the configuration information +of the application that is been wrapped by the Application Wrapper. This configuration could be +loaded from files or environment variables, or even both.

                  +

                  To understand the configuration loader options, check the documentation of the package +@mdf.js/service-setup-provider.

                  +

                  Use different files for Application Wrapper configuration and for your own services to +avoid conflicts.

                  +
                  consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>

                  OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 +consumer, that is used to receive and process OpenC2 commands. The consumer option in the +BootstrapOptions should be enabled to start the consumer.

                  +

                  Custom settings

                  +
                  loggerOptions?: Partial<LoggerConfig>

                  Logger Options. If provided, a logger instance from the @mdf.js/logger package will be +created and used by the application in all the internal services of the Application Wrapper. +At the same time, the logger is exposed to the application to be used in the application +services. If this options is not provided, a Debug logger will be used internally, but it +will not be exposed to the application.

                  +
                  metadata?: Partial<Metadata>

                  Metadata information of the application or microservice. This information is used to identify +the application in the logs, metrics, and traces... and is shown in the service observability +endpoints.

                  +
                  observabilityOptions?: Partial<ObservabilityServiceOptions>

                  Observability instance options

                  +
                  retryOptions?: Partial<
                      Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">,
                  >

                  Retry options. If provided, the application will use this options to retry to start the +services/resources registered in the Application Wrapped instance. If this options is not +provided, the application will not retry to start the services/resources.

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry._internal_.HealthRegistryOptions.html b/docs/interfaces/_mdf.js_service-registry._internal_.HealthRegistryOptions.html new file mode 100644 index 00000000..1e5abb5e --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry._internal_.HealthRegistryOptions.html @@ -0,0 +1,9 @@ +HealthRegistryOptions | @mdf.js
                  interface HealthRegistryOptions {
                      applicationMetadata: Metadata;
                      clusterUpdateInterval?: number;
                      isCluster?: boolean;
                      logger?: LoggerInstance;
                  }

                  Properties

                  applicationMetadata: Metadata

                  App health metadata properties

                  +
                  clusterUpdateInterval?: number

                  Cluster polling interval

                  +
                  isCluster?: boolean

                  Is the service running in cluster mode

                  +

                  Logger instance

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry._internal_.MetricsRegistryOptions.html b/docs/interfaces/_mdf.js_service-registry._internal_.MetricsRegistryOptions.html new file mode 100644 index 00000000..22d4d9a0 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry._internal_.MetricsRegistryOptions.html @@ -0,0 +1,15 @@ +MetricsRegistryOptions | @mdf.js
                  interface MetricsRegistryOptions {
                      instanceId: string;
                      isCluster?: boolean;
                      logger?: LoggerInstance;
                      name: string;
                  }

                  Properties

                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  +
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  +
                  + +
                  isCluster?: boolean

                  Is the service running in cluster mode

                  +

                  Logger instance

                  +
                  name: string

                  Service name

                  +
                  `myOwnService`
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_service-registry._internal_.ObservabilityOptions.html b/docs/interfaces/_mdf.js_service-registry._internal_.ObservabilityOptions.html new file mode 100644 index 00000000..8f7df0f1 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry._internal_.ObservabilityOptions.html @@ -0,0 +1,7 @@ +ObservabilityOptions | @mdf.js
                  interface ObservabilityOptions {
                      logger?: LoggerInstance;
                      metadata: Metadata;
                      service?: ObservabilityServiceOptions;
                  }

                  Properties

                  Properties

                  Logger instance

                  +
                  metadata: Metadata

                  Application metadata information

                  +

                  Observability service options

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-registry._internal_.RegistryOptions.html b/docs/interfaces/_mdf.js_service-registry._internal_.RegistryOptions.html new file mode 100644 index 00000000..b2ccb6f2 --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry._internal_.RegistryOptions.html @@ -0,0 +1,21 @@ +RegistryOptions | @mdf.js
                  interface RegistryOptions {
                      clusterUpdateInterval?: number;
                      includeStack?: boolean;
                      instanceId: string;
                      isCluster?: boolean;
                      logger?: LoggerInstance;
                      maxSize?: number;
                      name: string;
                  }

                  Properties

                  clusterUpdateInterval?: number

                  Cluster polling interval

                  +
                  includeStack?: boolean

                  Include stack trace in the error

                  +
                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  +
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  +
                  + +
                  isCluster?: boolean

                  Is the service running in cluster mode

                  +

                  Logger instance

                  +
                  maxSize?: number

                  Max size of the registry

                  +
                  name: string

                  Service name

                  +
                  `myOwnService`
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_service-registry._internal_.Response.html b/docs/interfaces/_mdf.js_service-registry._internal_.Response.html new file mode 100644 index 00000000..36bfb44d --- /dev/null +++ b/docs/interfaces/_mdf.js_service-registry._internal_.Response.html @@ -0,0 +1,5 @@ +Response | @mdf.js
                  interface Response {
                      contentType: string;
                      metrics: string | MetricObjectWithValues<MetricValue<string>>[];
                  }

                  Properties

                  Properties

                  contentType: string

                  Content type for HTTP headers

                  +
                  metrics: string | MetricObjectWithValues<MetricValue<string>>[]

                  Grouped metrics

                  +
                  diff --git a/docs/interfaces/_mdf.js_service-setup-provider.Setup.Config.html b/docs/interfaces/_mdf.js_service-setup-provider.Setup.Config.html new file mode 100644 index 00000000..37f0e71b --- /dev/null +++ b/docs/interfaces/_mdf.js_service-setup-provider.Setup.Config.html @@ -0,0 +1,51 @@ +Config | @mdf.js
                  interface Config<SystemConfig extends Record<string, any> = Record<string, any>> {
                      base?: Partial<SystemConfig>;
                      checker?: DoorKeeper<void>;
                      configFiles?: string[];
                      default?: Partial<SystemConfig>;
                      envPrefix?: string | string[] | Record<string, string>;
                      preset?: string;
                      presetFiles?: string[];
                      schema?: string;
                      schemaFiles?: string[];
                  }

                  Type Parameters

                  • SystemConfig extends Record<string, any> = Record<string, any>

                  Properties

                  base?: Partial<SystemConfig>

                  Object to be used as base and main configuration options. The configuration will be merged with +the configuration from the configuration files. This object will override the configuration +from the configuration files and the environment variables. The main reason of this option is +to allow the user to define some configuration in the code and let the rest of the +configuration to be loaded, using the Configuration Manager as unique source of configuration.

                  +
                  checker?: DoorKeeper<void>

                  DoorKeeper instance to be used to validate the configuration. If none is indicated, the setup +instance will be try to create a new DoorKeeper instance using the schema files indicated in +the options. If the schema files are not indicated, the configuration will not be validated.

                  +
                  configFiles?: string[]

                  List of configuration files to be loaded. The entries could be a file path or glob pattern. +All the files will be loaded and merged in the order they are founded. The result of the merge +will be used as the final configuration.

                  +
                  default?: Partial<SystemConfig>

                  Object to be used as default configuration options. The configuration will be merged with the +configuration from the configuration files, the environment variables and the base option. This +object will be used as the default configuration if no other configuration is found.

                  +
                  envPrefix?: string | string[] | Record<string, string>

                  Prefix or prefixes to use on configuration loading from the environment variables. The prefix +will be used to filter the environment variables. The prefix will be removed from the +environment variable name and the remaining part will be used as the configuration property +name. The configuration property name will be converted to camel case. +Environment variables will override the configuration from the configuration files.

                  +
                  `MY_APP_` // as single prefix
                  ['MY_APP_', 'MY_OTHER_APP_'] // as array of prefixes
                  { MY_APP: 'myApp', MY_OTHER_APP: 'myOtherApp' } // as object with prefixes +
                  + +
                  preset?: string

                  Preset to be used as configuration base, if none is indicated, or the indicated preset is +not found, the configuration from the configuration files will be used.

                  +
                  presetFiles?: string[]

                  List of files with preset options to be loaded. The entries could be a file path or glob +pattern. The first part of the file name will be used as the preset name. The file name +should be in the format of presetName.config.json or presetName.config.yaml. The name of +the preset will be used to merge different files in order to create a single preset.

                  +
                  `['./config/presets/*.json']`
                  +
                  + +
                  `['./config/presets/*.json', './config/presets/*.yaml']`
                  +
                  + +
                  `['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml']`
                  +
                  + +
                  schema?: string

                  Schema to be used to validate the configuration. If none is indicated, the configuration will +not be validated. The schema name should be the same as the file name without the extension.

                  +
                  schemaFiles?: string[]

                  List of files with JSON schemas used to validate the configuration. The entries could be a +file path or glob pattern.

                  +
                  diff --git a/docs/interfaces/_mdf.js_socket-client-provider.SocketIOClient.Config.html b/docs/interfaces/_mdf.js_socket-client-provider.SocketIOClient.Config.html new file mode 100644 index 00000000..53598dec --- /dev/null +++ b/docs/interfaces/_mdf.js_socket-client-provider.SocketIOClient.Config.html @@ -0,0 +1,3 @@ +Config | @mdf.js
                  interface Config {
                      url?: string;
                  }

                  Hierarchy

                  • Partial<ManagerOptions>
                  • Partial<SocketOptions>
                    • Config

                  Properties

                  Properties

                  url?: string

                  URL to connect to the server

                  +
                  diff --git a/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.BasicAuthentication.html b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.BasicAuthentication.html new file mode 100644 index 00000000..59f648d4 --- /dev/null +++ b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.BasicAuthentication.html @@ -0,0 +1,5 @@ +BasicAuthentication | @mdf.js

                  The basic authentication method

                  +
                  interface BasicAuthentication {
                      password: string;
                      type: "basic";
                      username: string;
                  }

                  Properties

                  Properties

                  password: string
                  type: "basic"
                  username: string
                  diff --git a/docs/interfaces/_mdf_js_socket_server_provider.SocketIOServer.Config.html b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.Config.html similarity index 51% rename from docs/interfaces/_mdf_js_socket_server_provider.SocketIOServer.Config.html rename to docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.Config.html index 9f226948..c47df0f8 100644 --- a/docs/interfaces/_mdf_js_socket_server_provider.SocketIOServer.Config.html +++ b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.Config.html @@ -1,9 +1,9 @@ -Config | @mdf.js
                  interface Config {
                      enableUI?: boolean;
                      host?: string;
                      port?: number;
                      ui?: InstrumentOptions;
                  }

                  Hierarchy

                  • Partial<ServerOptions>
                    • Config

                  Properties

                  Properties

                  enableUI?: boolean

                  Enable the admin UI

                  +Config | @mdf.js
                  interface Config {
                      enableUI?: boolean;
                      host?: string;
                      port?: number;
                      ui?: InstrumentOptions;
                  }

                  Hierarchy

                  • Partial<ServerOptions>
                    • Config

                  Properties

                  Properties

                  enableUI?: boolean

                  Enable the admin UI

                  host?: string

                  Server host

                  port?: number

                  Server port

                  -
                  ui?: InstrumentOptions

                  Interface UI options

                  -
                  +

                  Interface UI options

                  +
                  diff --git a/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.ConnectionError.html b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.ConnectionError.html new file mode 100644 index 00000000..39ce4184 --- /dev/null +++ b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.ConnectionError.html @@ -0,0 +1,12 @@ +ConnectionError | @mdf.js

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface ConnectionError {
                      code: number;
                      context: any;
                      message: string;
                      req: any;
                  }

                  Properties

                  Properties

                  code: number

                  Error code

                  +
                  context: any

                  Error context

                  +
                  message: string

                  Error message

                  +
                  req: any

                  Request object

                  +
                  diff --git a/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.InstrumentOptions.html b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.InstrumentOptions.html new file mode 100644 index 00000000..f719ceae --- /dev/null +++ b/docs/interfaces/_mdf.js_socket-server-provider.SocketIOServer.InstrumentOptions.html @@ -0,0 +1,21 @@ +InstrumentOptions | @mdf.js

                  The instrument options

                  +
                  interface InstrumentOptions {
                      auth?: false | BasicAuthentication;
                      mode: "development" | "production";
                      namespaceName: string;
                      readonly: boolean;
                      serverId?: string;
                      store: InMemoryStore | RedisStore;
                  }

                  Properties

                  auth?: false | BasicAuthentication

                  The authentication method

                  +
                  mode: "development" | "production"

                  Whether to send all events or only aggregated events to the UI, for performance purposes.

                  +
                  namespaceName: string

                  The name of the admin namespace

                  +
                  "/admin"
                  +
                  + +
                  readonly: boolean

                  Whether updates are allowed

                  +
                  false
                  +
                  + +
                  serverId?: string

                  The unique ID of the server

                  +

                  require("os").hostname()

                  +
                  store: InMemoryStore | RedisStore

                  The store

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.ConsolidatedLimiterOptions.html b/docs/interfaces/_mdf.js_tasks.ConsolidatedLimiterOptions.html new file mode 100644 index 00000000..f4be2cfd --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.ConsolidatedLimiterOptions.html @@ -0,0 +1,54 @@ +ConsolidatedLimiterOptions | @mdf.js

                  Interface ConsolidatedLimiterOptions

                  Represents the consolidated limiter options

                  +
                  interface ConsolidatedLimiterOptions {
                      autoStart: boolean;
                      bucketSize: number;
                      concurrency: number;
                      delay: number;
                      highWater: number;
                      interval: number;
                      penalty: number;
                      retryOptions?: RetryOptions;
                      strategy: Strategy;
                      tokensPerInterval: number;
                  }

                  Hierarchy

                  • Omit<Required<LimiterOptions>, "retryOptions">
                    • ConsolidatedLimiterOptions

                  Properties

                  autoStart: boolean

                  Set whether the limiter should start to process the jobs automatically

                  +
                  true
                  +
                  + +
                  bucketSize: number

                  Set the bucket size for the rate limiter

                  +

                  0 +If the bucket size is 0, only concurrency and delay will be used to limit the rate of the +jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to +limit the rate of the jobs. The bucket size is the maximum number of tokens that can be +consumed in the interval. The interval is defined by the tokensPerInterval and interval +properties.

                  +
                  concurrency: number

                  The maximum number of concurrent jobs

                  +
                  1
                  +
                  + +
                  delay: number

                  Delay between each job in milliseconds

                  +

                  0 +For concurrency = 1, the delay is applied after each job is finished +For concurrency > 1, if the actual number of concurrent jobs is less than concurrency, the +delay is applied after each job is finished, otherwise, the delay is applied after each job is +started.

                  +
                  highWater: number

                  The maximum number of jobs in the queue

                  +
                  Infinity
                  +
                  + +
                  interval: number

                  Define the interval in milliseconds

                  +
                  1000
                  +
                  + +
                  penalty: number

                  The penalty for the BLOCK strategy in milliseconds

                  +
                  0
                  +
                  + +
                  retryOptions?: RetryOptions
                  strategy: Strategy

                  The strategy to use when the queue length reaches highWater

                  +
                  'leak'
                  +
                  + +
                  tokensPerInterval: number

                  Define the number of tokens that will be added to the bucket at the beginning of the interval

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks.GroupTaskBaseConfig.html b/docs/interfaces/_mdf.js_tasks.GroupTaskBaseConfig.html new file mode 100644 index 00000000..5da32320 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.GroupTaskBaseConfig.html @@ -0,0 +1,6 @@ +GroupTaskBaseConfig | @mdf.js

                  Interface GroupTaskBaseConfig<Result, Binding>

                  Represents the base configuration for a group of tasks

                  +
                  interface GroupTaskBaseConfig<Result = any, Binding = any> {
                      options: WellIdentifiedTaskOptions<any>;
                      tasks: SingleTaskBaseConfig<Result, Binding>[];
                  }

                  Type Parameters

                  • Result = any
                  • Binding = any

                  Properties

                  Properties

                  Group of tasks options

                  +

                  Tasks

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.LimiterOptions.html b/docs/interfaces/_mdf.js_tasks.LimiterOptions.html new file mode 100644 index 00000000..ca2c4f44 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.LimiterOptions.html @@ -0,0 +1,58 @@ +LimiterOptions | @mdf.js

                  Interface LimiterOptions

                  Represents the limiter options

                  +
                  interface LimiterOptions {
                      autoStart?: boolean;
                      bucketSize?: number;
                      concurrency?: number;
                      delay?: number;
                      highWater?: number;
                      interval?: number;
                      penalty?: number;
                      retryOptions?: RetryOptions;
                      strategy?: Strategy;
                      tokensPerInterval?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  autoStart?: boolean

                  Set whether the limiter should start to process the jobs automatically

                  +
                  true
                  +
                  + +
                  bucketSize?: number

                  Set the bucket size for the rate limiter

                  +

                  0 +If the bucket size is 0, only concurrency and delay will be used to limit the rate of the +jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to +limit the rate of the jobs. The bucket size is the maximum number of tokens that can be +consumed in the interval. The interval is defined by the tokensPerInterval and interval +properties.

                  +
                  concurrency?: number

                  The maximum number of concurrent jobs

                  +
                  1
                  +
                  + +
                  delay?: number

                  Delay between each job in milliseconds

                  +

                  0 +For concurrency = 1, the delay is applied after each job is finished +For concurrency > 1, if the actual number of concurrent jobs is less than concurrency, the +delay is applied after each job is finished, otherwise, the delay is applied after each job is +started.

                  +
                  highWater?: number

                  The maximum number of jobs in the queue

                  +
                  Infinity
                  +
                  + +
                  interval?: number

                  Define the interval in milliseconds

                  +
                  1000
                  +
                  + +
                  penalty?: number

                  The penalty for the BLOCK strategy in milliseconds

                  +
                  0
                  +
                  + +
                  retryOptions?: RetryOptions

                  Set the default options for the retry process of the jobs

                  +
                  undefined
                  +
                  + +
                  strategy?: Strategy

                  The strategy to use when the queue length reaches highWater

                  +
                  'leak'
                  +
                  + +
                  tokensPerInterval?: number

                  Define the number of tokens that will be added to the bucket at the beginning of the interval

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks.MetaData.html b/docs/interfaces/_mdf.js_tasks.MetaData.html new file mode 100644 index 00000000..d9df539a --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.MetaData.html @@ -0,0 +1,29 @@ +MetaData | @mdf.js

                  Metadata of the execution of the task

                  +
                  interface MetaData {
                      $meta?: MetaData[];
                      cancelledAt?: string;
                      completedAt?: string;
                      createdAt: string;
                      duration?: number;
                      executedAt?: string;
                      failedAt?: string;
                      priority: number;
                      reason?: string;
                      status: TaskState;
                      taskId: string;
                      uuid: string;
                      weight: number;
                  }

                  Properties

                  $meta?: MetaData[]

                  Additional metadata objects, store the metadata information from related tasks in a sequence or +group

                  +
                  cancelledAt?: string

                  Date when the task was cancelled in ISO format

                  +
                  completedAt?: string

                  Date when the task was completed in ISO format

                  +
                  createdAt: string

                  Date when the task was created

                  +
                  duration?: number

                  Duration of the task in milliseconds

                  +
                  executedAt?: string

                  Date when the task was executed in ISO format

                  +
                  failedAt?: string

                  Date when the task was failed in ISO format

                  +
                  priority: number

                  Task priority

                  +
                  reason?: string

                  Reason of failure or cancellation

                  +
                  status: TaskState

                  Status of the task

                  +
                  taskId: string

                  Task identifier, defined by the user

                  +
                  uuid: string

                  Unique task identification, unique for each task

                  +
                  weight: number

                  Task weight

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.PollingManagerOptions.html b/docs/interfaces/_mdf.js_tasks.PollingManagerOptions.html new file mode 100644 index 00000000..332d067a --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.PollingManagerOptions.html @@ -0,0 +1,21 @@ +PollingManagerOptions | @mdf.js

                  Interface PollingManagerOptions

                  interface PollingManagerOptions {
                      componentId: string;
                      cyclesOnStats?: number;
                      entries: TaskBaseConfig[];
                      logger?: LoggerInstance;
                      pollingGroup: PollingGroup;
                      resource: string;
                      slowCycleRatio?: number;
                  }

                  Properties

                  componentId: string

                  Component identifier

                  +
                  cyclesOnStats?: number

                  Number of cycles on stats

                  +
                  10
                  +
                  + +
                  entries: TaskBaseConfig[]

                  Tasks configuration

                  +

                  Logger instance

                  +
                  pollingGroup: PollingGroup

                  Polling group assigned to this manager

                  +
                  resource: string

                  Resource identifier

                  +
                  slowCycleRatio?: number

                  Number of fast cycles to run per slow cycle

                  +
                  3
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks.PollingStats.html b/docs/interfaces/_mdf.js_tasks.PollingStats.html new file mode 100644 index 00000000..7f088538 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.PollingStats.html @@ -0,0 +1,28 @@ +PollingStats | @mdf.js

                  Interface PollingStats

                  Copyright 2024 Mytra Control S.L. All rights reserved.

                  +

                  Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                  +
                  interface PollingStats {
                      averageCycleDuration: number;
                      consecutiveOverruns: number;
                      cycles: number;
                      inFastCycleTasks: number;
                      inOffCycleTasks: number;
                      inSlowCycleTasks: number;
                      lastCycleDuration: number;
                      maxCycleDuration: number;
                      minCycleDuration: number;
                      overruns: number;
                      pendingTasks: number;
                      scanTime: Date;
                  }

                  Properties

                  averageCycleDuration: number

                  Average cycle duration in milliseconds

                  +
                  consecutiveOverruns: number

                  Number of consecutive overruns

                  +
                  cycles: number

                  Number of cycles performed

                  +
                  inFastCycleTasks: number

                  Number of tasks included on the fast cycle (normal cycle)

                  +
                  inOffCycleTasks: number

                  Number of tasks not included on the cycle

                  +
                  inSlowCycleTasks: number

                  Number of tasks included on the slow cycle

                  +
                  lastCycleDuration: number

                  Last cycle duration in milliseconds

                  +
                  maxCycleDuration: number

                  Maximum cycle duration in milliseconds

                  +
                  minCycleDuration: number

                  Minimum cycle duration in milliseconds

                  +
                  overruns: number

                  Number of cycles with overruns

                  +
                  pendingTasks: number

                  Number of pending tasks

                  +
                  scanTime: Date

                  Scan time

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.QueueOptions.html b/docs/interfaces/_mdf.js_tasks.QueueOptions.html new file mode 100644 index 00000000..e0334019 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.QueueOptions.html @@ -0,0 +1,36 @@ +QueueOptions | @mdf.js

                  Interface QueueOptions

                  Represents the queue options

                  +
                  interface QueueOptions {
                      bucketSize?: number;
                      highWater?: number;
                      interval?: number;
                      penalty?: number;
                      strategy?: Strategy;
                      tokensPerInterval?: number;
                  }

                  Hierarchy (View Summary)

                  Properties

                  bucketSize?: number

                  Set the bucket size for the rate limiter

                  +

                  0 +If the bucket size is 0, only concurrency and delay will be used to limit the rate of the +jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to +limit the rate of the jobs. The bucket size is the maximum number of tokens that can be +consumed in the interval. The interval is defined by the tokensPerInterval and interval +properties.

                  +
                  highWater?: number

                  The maximum number of jobs in the queue

                  +
                  Infinity
                  +
                  + +
                  interval?: number

                  Define the interval in milliseconds

                  +
                  1000
                  +
                  + +
                  penalty?: number

                  The penalty for the BLOCK strategy in milliseconds

                  +
                  0
                  +
                  + +
                  strategy?: Strategy

                  The strategy to use when the queue length reaches highWater

                  +
                  'leak'
                  +
                  + +
                  tokensPerInterval?: number

                  Define the number of tokens that will be added to the bucket at the beginning of the interval

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks.ResourceConfigEntry.html b/docs/interfaces/_mdf.js_tasks.ResourceConfigEntry.html new file mode 100644 index 00000000..e7c3e28b --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.ResourceConfigEntry.html @@ -0,0 +1,6 @@ +ResourceConfigEntry | @mdf.js

                  Interface ResourceConfigEntry<Result, Binding, PollingGroups>

                  Represents the resource configuration

                  +
                  interface ResourceConfigEntry<
                      Result = any,
                      Binding = any,
                      PollingGroups extends PollingGroup = DefaultPollingGroups,
                  > {
                      limiterOptions?: LimiterOptions;
                      pollingGroups: {
                          [polling in PollingGroup]?: TaskBaseConfig<Result, Binding>[]
                      };
                  }

                  Type Parameters

                  Properties

                  limiterOptions?: LimiterOptions

                  The limiter options

                  +
                  pollingGroups: { [polling in PollingGroup]?: TaskBaseConfig<Result, Binding>[] }

                  The polling groups

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.ResourcesConfigObject.html b/docs/interfaces/_mdf.js_tasks.ResourcesConfigObject.html new file mode 100644 index 00000000..8c21f1be --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.ResourcesConfigObject.html @@ -0,0 +1,4 @@ +ResourcesConfigObject | @mdf.js

                  Interface ResourcesConfigObject<Result, Binding, PollingGroups>

                  Represents the resources object, a map of resources with their polling groups and the tasks to +execute in that polling groups

                  +

                  Type Parameters

                  Indexable

                  diff --git a/docs/interfaces/_mdf.js_tasks.SchedulerOptions.html b/docs/interfaces/_mdf.js_tasks.SchedulerOptions.html new file mode 100644 index 00000000..3e2612b1 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.SchedulerOptions.html @@ -0,0 +1,18 @@ +SchedulerOptions | @mdf.js

                  Interface SchedulerOptions<Result, Binding, PollingGroups>

                  Represents the options for the scheduler

                  +
                  interface SchedulerOptions<
                      Result = any,
                      Binding = any,
                      PollingGroups extends PollingGroup = DefaultPollingGroups,
                  > {
                      cyclesOnStats?: number;
                      limiterOptions?: LimiterOptions;
                      logger?: LoggerInstance;
                      resources?: ResourcesConfigObject<Result, Binding, PollingGroups>;
                      slowCycleRatio?: number;
                  }

                  Type Parameters

                  Properties

                  cyclesOnStats?: number

                  Number of cycles on stats

                  +
                  10
                  +
                  + +
                  limiterOptions?: LimiterOptions

                  The limiter options

                  +

                  The logger for the scheduler

                  +

                  The entries for the scheduler

                  +
                  slowCycleRatio?: number

                  Number of fast cycles to run per slow cycle

                  +
                  3
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf_js_tasks.SequencePattern.html b/docs/interfaces/_mdf.js_tasks.SequencePattern.html similarity index 52% rename from docs/interfaces/_mdf_js_tasks.SequencePattern.html rename to docs/interfaces/_mdf.js_tasks.SequencePattern.html index 4c45bc5c..53860ee7 100644 --- a/docs/interfaces/_mdf_js_tasks.SequencePattern.html +++ b/docs/interfaces/_mdf.js_tasks.SequencePattern.html @@ -1,12 +1,12 @@ -SequencePattern | @mdf.js

                  Interface SequencePattern<T>

                  Represents the pattern for task execution as a sequence of tasks

                  -
                  interface SequencePattern<T> {
                      finally?: TaskHandler<any, any>[];
                      post?: TaskHandler<any, any>[];
                      pre?: TaskHandler<any, any>[];
                      task: TaskHandler<T, any>;
                  }

                  Type Parameters

                  • T = any

                  Properties

                  Properties

                  finally?: TaskHandler<any, any>[]

                  Tasks to be executed at the end of the sequence, the finally tasks will be executed even if the +SequencePattern | @mdf.js

                  Interface SequencePattern<T>

                  Represents the pattern for task execution as a sequence of tasks

                  +
                  interface SequencePattern<T = any> {
                      finally?: TaskHandler<any, any>[];
                      post?: TaskHandler<any, any>[];
                      pre?: TaskHandler<any, any>[];
                      task: TaskHandler<T, any>;
                  }

                  Type Parameters

                  • T = any

                  Properties

                  Properties

                  finally?: TaskHandler<any, any>[]

                  Tasks to be executed at the end of the sequence, the finally tasks will be executed even if the main task fails.

                  -
                  post?: TaskHandler<any, any>[]

                  Tasks to be executed after the main task, if the main task fails, the post tasks will not be +

                  post?: TaskHandler<any, any>[]

                  Tasks to be executed after the main task, if the main task fails, the post tasks will not be executed, but the finally tasks will be executed.

                  -
                  pre?: TaskHandler<any, any>[]

                  Tasks to be executed before the main task

                  -
                  task: TaskHandler<T, any>

                  The main task to be executed

                  -
                  +
                  pre?: TaskHandler<any, any>[]

                  Tasks to be executed before the main task

                  +
                  task: TaskHandler<T, any>

                  The main task to be executed

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.SequenceTaskBaseConfig.html b/docs/interfaces/_mdf.js_tasks.SequenceTaskBaseConfig.html new file mode 100644 index 00000000..ada467f4 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.SequenceTaskBaseConfig.html @@ -0,0 +1,6 @@ +SequenceTaskBaseConfig | @mdf.js

                  Interface SequenceTaskBaseConfig<Result, Binding>

                  Represents the base configuration for a sequence of tasks

                  +
                  interface SequenceTaskBaseConfig<Result = any, Binding = any> {
                      options: WellIdentifiedTaskOptions<any>;
                      pattern: {
                          finally?: SingleTaskBaseConfig<Result, Binding>[];
                          post?: SingleTaskBaseConfig<Result, Binding>[];
                          pre?: SingleTaskBaseConfig<Result, Binding>[];
                          task: SingleTaskBaseConfig<Result, Binding>;
                      };
                  }

                  Type Parameters

                  • Result = any
                  • Binding = any

                  Properties

                  Properties

                  The schedule of the task

                  +
                  pattern: {
                      finally?: SingleTaskBaseConfig<Result, Binding>[];
                      post?: SingleTaskBaseConfig<Result, Binding>[];
                      pre?: SingleTaskBaseConfig<Result, Binding>[];
                      task: SingleTaskBaseConfig<Result, Binding>;
                  }

                  Task pattern

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.SingleTaskBaseConfig.html b/docs/interfaces/_mdf.js_tasks.SingleTaskBaseConfig.html new file mode 100644 index 00000000..de816a7e --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.SingleTaskBaseConfig.html @@ -0,0 +1,8 @@ +SingleTaskBaseConfig | @mdf.js

                  Interface SingleTaskBaseConfig<Result, Binding>

                  Represents the base configuration for a single task

                  +
                  interface SingleTaskBaseConfig<Result = any, Binding = any> {
                      options: WellIdentifiedTaskOptions<Binding>;
                      task: TaskAsPromise<Result>;
                      taskArgs?: TaskArguments;
                  }

                  Type Parameters

                  • Result = any
                  • Binding = any

                  Properties

                  Properties

                  Task options

                  +

                  Task

                  +
                  taskArgs?: TaskArguments

                  Task arguments

                  +
                  diff --git a/docs/interfaces/_mdf.js_tasks.TaskOptions.html b/docs/interfaces/_mdf.js_tasks.TaskOptions.html new file mode 100644 index 00000000..0f268da1 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.TaskOptions.html @@ -0,0 +1,32 @@ +TaskOptions | @mdf.js

                  Interface TaskOptions<U>

                  Represents the options for a task

                  +
                  interface TaskOptions<U> {
                      bind?: U;
                      id?: string;
                      priority?: number;
                      retryOptions?: RetryOptions;
                      retryStrategy?: RetryStrategy;
                      weight?: number;
                  }

                  Type Parameters

                  • U

                  Hierarchy (View Summary)

                  Properties

                  bind?: U

                  Context to be bind to the task

                  +
                  id?: string

                  Task identifier, it necessary to identify the task during all the process, for example, when +the job is executed, the event with the task identifier will be emitted with the result of the +task.

                  +
                  If not provided, the task identifier will be generated automatically.
                  +
                  + +
                  priority?: number

                  The priority of the task. A higher value means a higher priority. The default priority is 0.

                  +
                  0
                  +
                  + +
                  retryOptions?: RetryOptions

                  Set the options for the retry process of the task

                  +
                  undefined
                  +
                  + +
                  retryStrategy?: RetryStrategy

                  Set the strategy to retry the task

                  +
                  RETRY_STRATEGY.RETRY
                  +
                  + +
                  weight?: number

                  The weight of the task, this define the number of tokens that the task will consume from the +bucket. The default weight is 1.

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks.WellIdentifiedTaskOptions.html b/docs/interfaces/_mdf.js_tasks.WellIdentifiedTaskOptions.html new file mode 100644 index 00000000..1333ae57 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks.WellIdentifiedTaskOptions.html @@ -0,0 +1,29 @@ +WellIdentifiedTaskOptions | @mdf.js

                  Interface WellIdentifiedTaskOptions<Binding>

                  Extends the task options with the task identifier, making it mandatory

                  +
                  interface WellIdentifiedTaskOptions<Binding = any> {
                      bind?: Binding;
                      id: string;
                      priority?: number;
                      retryOptions?: RetryOptions;
                      retryStrategy?: RetryStrategy;
                      weight?: number;
                  }

                  Type Parameters

                  • Binding = any

                  Hierarchy (View Summary)

                  Properties

                  bind?: Binding

                  Context to be bind to the task

                  +
                  id: string

                  Task identifier, it necessary to identify the task during all the process, for example, when +the job is executed, the event with the task identifier will be emitted with the result of the +task.

                  +
                  priority?: number

                  The priority of the task. A higher value means a higher priority. The default priority is 0.

                  +
                  0
                  +
                  + +
                  retryOptions?: RetryOptions

                  Set the options for the retry process of the task

                  +
                  undefined
                  +
                  + +
                  retryStrategy?: RetryStrategy

                  Set the strategy to retry the task

                  +
                  RETRY_STRATEGY.RETRY
                  +
                  + +
                  weight?: number

                  The weight of the task, this define the number of tokens that the task will consume from the +bucket. The default weight is 1.

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_tasks._internal_.ConsolidatedQueueOptions.html b/docs/interfaces/_mdf.js_tasks._internal_.ConsolidatedQueueOptions.html new file mode 100644 index 00000000..df96dd28 --- /dev/null +++ b/docs/interfaces/_mdf.js_tasks._internal_.ConsolidatedQueueOptions.html @@ -0,0 +1,36 @@ +ConsolidatedQueueOptions | @mdf.js

                  Represents the consolidated limiter options

                  +
                  interface ConsolidatedQueueOptions {
                      bucketSize: number;
                      highWater: number;
                      interval: number;
                      penalty: number;
                      strategy: Strategy;
                      tokensPerInterval: number;
                  }

                  Hierarchy

                  Properties

                  bucketSize: number

                  Set the bucket size for the rate limiter

                  +

                  0 +If the bucket size is 0, only concurrency and delay will be used to limit the rate of the +jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to +limit the rate of the jobs. The bucket size is the maximum number of tokens that can be +consumed in the interval. The interval is defined by the tokensPerInterval and interval +properties.

                  +
                  highWater: number

                  The maximum number of jobs in the queue

                  +
                  Infinity
                  +
                  + +
                  interval: number

                  Define the interval in milliseconds

                  +
                  1000
                  +
                  + +
                  penalty: number

                  The penalty for the BLOCK strategy in milliseconds

                  +
                  0
                  +
                  + +
                  strategy: Strategy

                  The strategy to use when the queue length reaches highWater

                  +
                  'leak'
                  +
                  + +
                  tokensPerInterval: number

                  Define the number of tokens that will be added to the bucket at the beginning of the interval

                  +
                  1
                  +
                  + +
                  diff --git a/docs/interfaces/_mdf.js_utils.ReadEnvOptions.html b/docs/interfaces/_mdf.js_utils.ReadEnvOptions.html new file mode 100644 index 00000000..dd18c281 --- /dev/null +++ b/docs/interfaces/_mdf.js_utils.ReadEnvOptions.html @@ -0,0 +1,8 @@ +ReadEnvOptions | @mdf.js

                  Interface ReadEnvOptions

                  Read environment options

                  +
                  interface ReadEnvOptions {
                      format: Format | FormatFunction;
                      includePrefix: boolean;
                      separator: string;
                  }

                  Properties

                  Format

                  +
                  includePrefix: boolean

                  Include prefix

                  +
                  separator: string

                  Environment variable separator

                  +
                  diff --git a/docs/interfaces/_mdf.js_utils.RetryOptions.html b/docs/interfaces/_mdf.js_utils.RetryOptions.html new file mode 100644 index 00000000..7f7951af --- /dev/null +++ b/docs/interfaces/_mdf.js_utils.RetryOptions.html @@ -0,0 +1,18 @@ +RetryOptions | @mdf.js

                  Interface RetryOptions

                  Represents the options for retrying an operation

                  +
                  interface RetryOptions {
                      abortSignal?: AbortSignal;
                      attempts?: number;
                      interrupt?: () => boolean;
                      logger?: LoggerFunction;
                      maxWaitTime?: number;
                      timeout?: number;
                      waitTime?: number;
                  }

                  Properties

                  abortSignal?: AbortSignal

                  The signal to be used to interrupt the retry process

                  +
                  attempts?: number

                  The maximum number of retry attempts.

                  +
                  interrupt?: () => boolean

                  A function that determines whether to interrupt the retry process +Should return true to interrupt, false otherwise.

                  +

                  User abortSignal instead

                  +

                  The logger function used for logging retry attempts

                  +
                  maxWaitTime?: number

                  The maximum time to wait between retry attempts, in milliseconds

                  +
                  timeout?: number

                  Timeout for each try

                  +
                  waitTime?: number

                  The time to wait between retry attempts, in milliseconds

                  +
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.DefaultOptions.html b/docs/interfaces/_mdf_js_core.Jobs.DefaultOptions.html deleted file mode 100644 index 06bd48fb..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.DefaultOptions.html +++ /dev/null @@ -1,6 +0,0 @@ -DefaultOptions | @mdf.js

                  Interface DefaultOptions<CustomHeaders>

                  interface DefaultOptions<CustomHeaders> {
                      headers?: CustomHeaders;
                      numberOfHandlers?: number;
                  }

                  Type Parameters

                  • CustomHeaders extends Record<string, any> = AnyHeaders

                  Properties

                  headers?: CustomHeaders

                  Job meta information, used to pass specific information for jobs handlers

                  -
                  numberOfHandlers?: number

                  Indicates the number of handlers that must be successfully processed to consider the job as -successfully processed

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.JobObject.html b/docs/interfaces/_mdf_js_core.Jobs.JobObject.html deleted file mode 100644 index 0f141fbc..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.JobObject.html +++ /dev/null @@ -1,16 +0,0 @@ -JobObject | @mdf.js

                  Interface JobObject<Type, Data, CustomHeaders, CustomOptions>

                  Job object

                  -
                  interface JobObject<Type, Data, CustomHeaders, CustomOptions> {
                      data: Data;
                      jobUserId: string;
                      jobUserUUID: string;
                      options?: Options<CustomHeaders, CustomOptions>;
                      status: Jobs.Status;
                      type: Type;
                      uuid: string;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (view full)

                  Implemented by

                  Properties

                  data: Data

                  Job payload

                  -
                  jobUserId: string

                  User job request identifier, defined by the user

                  -
                  jobUserUUID: string

                  Unique user job request identification, generated by UUID V5 standard and based on jobUserId

                  -

                  Job meta information, used to pass specific information for job processors

                  -
                  status: Jobs.Status

                  Job status

                  -
                  type: Type

                  Job type identification, used to identify specific job handlers to be applied

                  -
                  uuid: string

                  Unique job processing identification

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.JobRequest.html b/docs/interfaces/_mdf_js_core.Jobs.JobRequest.html deleted file mode 100644 index 92beab7e..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.JobRequest.html +++ /dev/null @@ -1,9 +0,0 @@ -JobRequest | @mdf.js

                  Interface JobRequest<Type, Data, CustomHeaders, CustomOptions>

                  interface JobRequest<Type, Data, CustomHeaders, CustomOptions> {
                      data: Data;
                      jobUserId: string;
                      options?: Options<CustomHeaders, CustomOptions>;
                      type?: Type;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = unknown
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Hierarchy (view full)

                  Properties

                  Properties

                  data: Data

                  Job payload

                  -
                  jobUserId: string

                  User job request identifier, defined by the user

                  -

                  Job meta information, used to pass specific information for job processors

                  -
                  type?: Type

                  Job type identification, used to identify specific job handlers to be applied

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.NoMoreHeaders.html b/docs/interfaces/_mdf_js_core.Jobs.NoMoreHeaders.html deleted file mode 100644 index ed2b92d4..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.NoMoreHeaders.html +++ /dev/null @@ -1,2 +0,0 @@ -NoMoreHeaders | @mdf.js

                  No more extra headers information

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.NoMoreOptions.html b/docs/interfaces/_mdf_js_core.Jobs.NoMoreOptions.html deleted file mode 100644 index 53186c91..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.NoMoreOptions.html +++ /dev/null @@ -1,2 +0,0 @@ -NoMoreOptions | @mdf.js

                  No more extra options information

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.Result.html b/docs/interfaces/_mdf_js_core.Jobs.Result.html deleted file mode 100644 index 4ab0b4a0..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.Result.html +++ /dev/null @@ -1,22 +0,0 @@ -Result | @mdf.js

                  Interface Result<Type>

                  Job result interface

                  -
                  interface Result<Type> {
                      createdAt: string;
                      errors?: MultiObject;
                      hasErrors: boolean;
                      jobUserId: string;
                      jobUserUUID: string;
                      quantity: number;
                      resolvedAt: string;
                      status: Jobs.Status;
                      type: Type;
                      uuid: string;
                  }

                  Type Parameters

                  • Type extends string = string

                  Properties

                  createdAt: string

                  Timestamp, in ISO format, of the job creation date

                  -
                  errors?: MultiObject

                  Array of errors

                  -
                  hasErrors: boolean

                  Flag that indicate that the publication process has some errors

                  -
                  jobUserId: string

                  User job request identifier, defined by the user

                  -
                  jobUserUUID: string

                  Unique user job request identification, based on jobUserId

                  -
                  quantity: number

                  Number of entities processed with success in this job

                  -
                  resolvedAt: string

                  Timestamp, in ISO format, of the job resolve date

                  -
                  status: Jobs.Status

                  Job status

                  -
                  type: Type

                  Job type

                  -
                  uuid: string

                  Unique job processing identification

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Jobs.Strategy.html b/docs/interfaces/_mdf_js_core.Jobs.Strategy.html deleted file mode 100644 index c840a53a..00000000 --- a/docs/interfaces/_mdf_js_core.Jobs.Strategy.html +++ /dev/null @@ -1,7 +0,0 @@ -Strategy | @mdf.js

                  Interface Strategy<Type, Data, CustomHeaders, CustomOptions>

                  Base class for strategies

                  -
                  interface Strategy<Type, Data, CustomHeaders, CustomOptions> {
                      do: ((process: JobObject<Type, Data, CustomHeaders, CustomOptions>) => JobObject<Type, Data, CustomHeaders, CustomOptions>);
                      name: string;
                  }

                  Type Parameters

                  • Type extends string = string
                  • Data = any
                  • CustomHeaders extends Record<string, any> = AnyHeaders
                  • CustomOptions extends Record<string, any> = AnyOptions

                  Properties

                  do -name -

                  Properties

                  Perform the filter of the data based in concrete criteria

                  -

                  Type declaration

                  name: string

                  Strategy name

                  -
                  diff --git a/docs/interfaces/_mdf_js_core.Layer.App.Component.html b/docs/interfaces/_mdf_js_core.Layer.App.Component.html deleted file mode 100644 index 719f849f..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.App.Component.html +++ /dev/null @@ -1,12 +0,0 @@ -Component | @mdf.js

                  A component is any part of the system that has a own identity and can be monitored for error -handling. The only requirement is to emit an error event when something goes wrong, to have a -name and unique component identifier.

                  -
                  interface Component {
                      componentId: string;
                      name: string;
                      on(event: "error", listener: ((error: Multi | Error | Crash) => void)): this;
                  }

                  Hierarchy (view full)

                  Properties

                  Methods

                  on -

                  Properties

                  componentId: string

                  Component identifier

                  -
                  name: string

                  Component name

                  -

                  Methods

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Multi | Error | Crash) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  diff --git a/docs/interfaces/_mdf_js_core.Layer.App.Health.html b/docs/interfaces/_mdf_js_core.Layer.App.Health.html deleted file mode 100644 index f13b1f0b..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.App.Health.html +++ /dev/null @@ -1,101 +0,0 @@ -Health | @mdf.js

                  Health service interface

                  -
                  interface Health {
                      checks?: Checks;
                      description?: string;
                      instanceId: string;
                      links?: {
                          about?: string;
                          related?: string;
                          self?: string;
                      };
                      name: string;
                      namespace?: `x-${string}`;
                      notes?: string[];
                      output?: string;
                      release: string;
                      serviceGroupId?: string;
                      serviceId?: string;
                      status: "pass" | "fail" | "warn";
                      tags?: string[];
                      version: string;
                  }

                  Hierarchy (view full)

                  Properties

                  checks?: Checks

                  The “checks” object MAY have a number of unique keys, one for each logical sub-components. -Since each sub-component may be backed by several nodes with varying health statuses, the key -points to an array of objects. In case of a single-node sub-component (or if presence of nodes -is not relevant), a single-element array should be used as the value, for consistency. -The key identifying an element in the object should be a unique string within the details -section. It MAY have two parts: {componentName}:{metricName}, in which case the meaning of -the parts SHOULD be as follows:

                  -
                    -
                  • componentName: Human-readable name for the component. MUST not contain a colon, in the name, -since colon is used as a separator
                  • -
                  • metricName: Name of the metrics that the status is reported for. MUST not contain a colon, -in the name, since colon is used as a separator and can be one of: -
                      -
                    • Pre-defined value from this spec. Pre-defined values include: -
                        -
                      • utilization
                      • -
                      • responseTime
                      • -
                      • connections
                      • -
                      • uptime
                      • -
                      -
                    • -
                    • A common and standard term from a well-known source such as schema.org, IANA or -microformats.
                    • -
                    • A URI that indicates extra semantics and processing rules that MAY be provided by a -resource at the other end of the URI. URIs do not have to be dereferenceable, however. -They are just a namespace, and the meaning of a namespace CAN be provided by any -convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book).
                    • -
                    -
                  • -
                  -
                  description?: string

                  Service description

                  -
                  `My own service description`
                  -
                  - -
                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  -
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  -
                  - -
                  links?: {
                      about?: string;
                      related?: string;
                      self?: string;
                  }

                  Service related links

                  -

                  Type declaration

                  • Optionalabout?: string

                    About link for the service

                    -
                    `https://www.mytra.es/about`
                    -
                    - -
                  • Optionalrelated?: string

                    Related link for the service

                    -
                    `https://www.mytra.es`
                    -
                    - -
                  • Optionalself?: string

                    Link to the own service or health endpoint

                    -
                    `http://localhost:3000/v1/health`
                    -
                    - -
                  name: string

                  Service name

                  -
                  `myOwnService`
                  -
                  - -
                  namespace?: `x-${string}`

                  Service namespace, used to identify declare which namespace the service belongs to. -It must start with x- as it is a custom namespace and will be used for custom headers, -openc2 commands, etc.

                  -
                  `x-mytra`
                  -
                  - -
                  notes?: string[]

                  Array of notes relevant to current state of health

                  -
                  output?: string

                  Raw error output, in case of “fail” or “warn” states. This field SHOULD be omitted for -“pass” state.

                  -
                  release: string

                  Service release. Its recommended to use semantic versioning.

                  -
                  `1.0.0`
                  -
                  - -
                  serviceGroupId?: string

                  Service group unique identification

                  -
                  `firehose`, `driver`
                  -
                  - -
                  serviceId?: string

                  Service unique identification

                  -
                  `uplink-firehose`, `mqtt-driver`
                  -
                  - -
                  status: "pass" | "fail" | "warn"

                  Indicates whether the service status is acceptable or not

                  -
                  tags?: string[]

                  List of string values that can be used to add service-level labels.

                  -
                  `["primary", "test"]`
                  -
                  - -
                  version: string

                  Service version

                  -
                  `1`
                  -
                  - -
                  diff --git a/docs/interfaces/_mdf_js_core.Layer.App.Metadata.html b/docs/interfaces/_mdf_js_core.Layer.App.Metadata.html deleted file mode 100644 index d62f8a62..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.App.Metadata.html +++ /dev/null @@ -1,63 +0,0 @@ -Metadata | @mdf.js

                  Application definition

                  -
                  interface Metadata {
                      description?: string;
                      instanceId: string;
                      links?: {
                          about?: string;
                          related?: string;
                          self?: string;
                      };
                      name: string;
                      namespace?: `x-${string}`;
                      release: string;
                      serviceGroupId?: string;
                      serviceId?: string;
                      tags?: string[];
                      version: string;
                  }

                  Hierarchy (view full)

                  Properties

                  description?: string

                  Service description

                  -
                  `My own service description`
                  -
                  - -
                  instanceId: string

                  Service instance unique identification within the scope of the service identification

                  -
                  `085f47e9-7fad-4da1-b5e5-31fc6eed9f94`
                  -
                  - -
                  links?: {
                      about?: string;
                      related?: string;
                      self?: string;
                  }

                  Service related links

                  -

                  Type declaration

                  • Optionalabout?: string

                    About link for the service

                    -
                    `https://www.mytra.es/about`
                    -
                    - -
                  • Optionalrelated?: string

                    Related link for the service

                    -
                    `https://www.mytra.es`
                    -
                    - -
                  • Optionalself?: string

                    Link to the own service or health endpoint

                    -
                    `http://localhost:3000/v1/health`
                    -
                    - -
                  name: string

                  Service name

                  -
                  `myOwnService`
                  -
                  - -
                  namespace?: `x-${string}`

                  Service namespace, used to identify declare which namespace the service belongs to. -It must start with x- as it is a custom namespace and will be used for custom headers, -openc2 commands, etc.

                  -
                  `x-mytra`
                  -
                  - -
                  release: string

                  Service release. Its recommended to use semantic versioning.

                  -
                  `1.0.0`
                  -
                  - -
                  serviceGroupId?: string

                  Service group unique identification

                  -
                  `firehose`, `driver`
                  -
                  - -
                  serviceId?: string

                  Service unique identification

                  -
                  `uplink-firehose`, `mqtt-driver`
                  -
                  - -
                  tags?: string[]

                  List of string values that can be used to add service-level labels.

                  -
                  `["primary", "test"]`
                  -
                  - -
                  version: string

                  Service version

                  -
                  `1`
                  -
                  - -
                  diff --git a/docs/interfaces/_mdf_js_core.Layer.App.Resource.html b/docs/interfaces/_mdf_js_core.Layer.App.Resource.html deleted file mode 100644 index 49a222f2..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.App.Resource.html +++ /dev/null @@ -1,28 +0,0 @@ -Resource | @mdf.js

                  A resource is extended component that represent the access to an external/internal resource, -besides the error handling and identity, it has a start, stop and close methods to manage the -resource lifecycle. It also has a checks property to define the checks that will be performed -over the resource to achieve the resulted status. -The most typical example of a resource are the Provider that allow to access to external -databases, message brokers, etc.

                  -
                  interface Resource {
                      checks: Checks;
                      close: (() => Promise<void>);
                      componentId: string;
                      name: string;
                      start: (() => Promise<void>);
                      status: "pass" | "fail" | "warn";
                      stop: (() => Promise<void>);
                      on(event: "error", listener: ((error: Multi | Error | Crash) => void)): this;
                      on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                  }

                  Hierarchy (view full)

                  Implemented by

                  Properties

                  Methods

                  on -

                  Properties

                  checks: Checks

                  Checks performed over this component to achieve the resulted status

                  -
                  close: (() => Promise<void>)

                  Resource close function

                  -
                  componentId: string

                  Component identifier

                  -
                  name: string

                  Component name

                  -
                  start: (() => Promise<void>)

                  Resource start function

                  -
                  status: "pass" | "fail" | "warn"

                  Resource status

                  -
                  stop: (() => Promise<void>)

                  Resource stop function

                  -

                  Methods

                  • Add a listener for the error event, emitted when the component detects an error.

                    -

                    Parameters

                    • event: "error"

                      error event

                      -
                    • listener: ((error: Multi | Error | Crash) => void)

                      Error event listener

                      -
                        • (error): void
                        • Parameters

                          Returns void

                    Returns this

                  • Add a listener for the status event, emitted when the component status changes.

                    -

                    Parameters

                    • event: "status"

                      status event

                      -
                    • listener: ((status: "pass" | "fail" | "warn") => void)

                      Status event listener

                      -
                        • (status): void
                        • Parameters

                          • status: "pass" | "fail" | "warn"

                          Returns void

                    Returns this

                  diff --git a/docs/interfaces/_mdf_js_core.Layer.App.Service.html b/docs/interfaces/_mdf_js_core.Layer.App.Service.html deleted file mode 100644 index cf8d4fc6..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.App.Service.html +++ /dev/null @@ -1,37 +0,0 @@ -Service | @mdf.js

                  A service is a special kind of resource that besides Resource properties, it could offer:

                  -
                    -
                  • Its own REST API endpoints, using an express router, to expose details about service, this -endpoints will be exposed under the observability paths.
                  • -
                  • A links property to define the endpoints that the service expose, this information will be -exposed in the observability paths.
                  • -
                  • A metrics property to expose the metrics of the service, this registry will be merged with the -global metrics registry.
                  • -
                  -
                  interface Service {
                      checks: Checks;
                      close: (() => Promise<void>);
                      componentId: string;
                      links?: Links;
                      metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                      name: string;
                      router?: Router;
                      start: (() => Promise<void>);
                      status: "pass" | "fail" | "warn";
                      stop: (() => Promise<void>);
                      on(event: "error", listener: ((error: Multi | Error | Crash) => void)): this;
                      on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                  }

                  Hierarchy (view full)

                  Implemented by

                    Properties

                    checks: Checks

                    Checks performed over this component to achieve the resulted status

                    -
                    close: (() => Promise<void>)

                    Resource close function

                    -
                    componentId: string

                    Component identifier

                    -
                    links?: Links

                    Service base path

                    -
                    metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                    Get the metrics registry

                    -
                    name: string

                    Component name

                    -
                    router?: Router

                    Express router

                    -
                    start: (() => Promise<void>)

                    Resource start function

                    -
                    status: "pass" | "fail" | "warn"

                    Resource status

                    -
                    stop: (() => Promise<void>)

                    Resource stop function

                    -

                    Methods

                    • Add a listener for the error event, emitted when the component detects an error.

                      -

                      Parameters

                      • event: "error"

                        error event

                        -
                      • listener: ((error: Multi | Error | Crash) => void)

                        Error event listener

                        -
                          • (error): void
                          • Parameters

                            Returns void

                      Returns this

                    • Add a listener for the status event, emitted when the component status changes.

                      -

                      Parameters

                      • event: "status"

                        status event

                        -
                      • listener: ((status: "pass" | "fail" | "warn") => void)

                        Status event listener

                        -
                          • (status): void
                          • Parameters

                            • status: "pass" | "fail" | "warn"

                            Returns void

                      Returns this

                    diff --git a/docs/interfaces/_mdf_js_core.Layer.Provider.Factory.html b/docs/interfaces/_mdf_js_core.Layer.Provider.Factory.html deleted file mode 100644 index da144cfb..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.Provider.Factory.html +++ /dev/null @@ -1,5 +0,0 @@ -Factory | @mdf.js

                    Interface Factory<PortClient, PortConfig, T>

                    Provider factory interface

                    -
                    interface Factory<PortClient, PortConfig, T> {
                        create(options?: FactoryOptions<PortConfig>): Manager<PortClient, PortConfig, T>;
                    }

                    Type Parameters

                    Methods

                    Methods

                    diff --git a/docs/interfaces/_mdf_js_core.Layer.Provider.PortConfigValidationStruct.html b/docs/interfaces/_mdf_js_core.Layer.Provider.PortConfigValidationStruct.html deleted file mode 100644 index 29184266..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.Provider.PortConfigValidationStruct.html +++ /dev/null @@ -1,9 +0,0 @@ -PortConfigValidationStruct | @mdf.js

                    Interface PortConfigValidationStruct<PortConfig>

                    Port configuration validation structure

                    -
                    interface PortConfigValidationStruct<PortConfig> {
                        defaultConfig: PortConfig;
                        envBasedConfig: PortConfig;
                        schema: Schema<PortConfig>;
                    }

                    Type Parameters

                    • PortConfig

                      Port configuration object, could be an extended version of the client config

                      -

                    Properties

                    defaultConfig: PortConfig

                    Default configuration options

                    -
                    envBasedConfig: PortConfig

                    Environment based configuration options

                    -
                    schema: Schema<PortConfig>

                    Schema for configuration validation

                    -
                    diff --git a/docs/interfaces/_mdf_js_core.Layer.Provider.ProviderOptions.html b/docs/interfaces/_mdf_js_core.Layer.Provider.ProviderOptions.html deleted file mode 100644 index ed2fa63f..00000000 --- a/docs/interfaces/_mdf_js_core.Layer.Provider.ProviderOptions.html +++ /dev/null @@ -1,25 +0,0 @@ -ProviderOptions | @mdf.js

                    Interface ProviderOptions<PortConfig>

                    Provider configuration options

                    -
                    interface ProviderOptions<PortConfig> {
                        logger?: LoggerInstance;
                        name: string;
                        type: string;
                        useEnvironment?: string | boolean;
                        validation: PortConfigValidationStruct<PortConfig>;
                    }

                    Type Parameters

                    • PortConfig

                      Port configuration object, could be an extended version of the client config

                      -

                    Properties

                    Port and provider logger, to be used internally

                    -
                    name: string

                    Provider name, used for human-readable logs and identification

                    -
                    type: string

                    Provider type, kind of component form the points of view of the health check standard -https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06

                    -
                    useEnvironment?: string | boolean

                    String is used as prefix for the environment configuration variables, represented in -SCREAMING_SNAKE_CASE, that will parsed to camelCase and merged with the rest of the -configuration.

                    -
                    // Environment variables
                    process.env.PORT_NAME_TYPE = 'http';
                    process.env.PORT_NAME_HOST = 'localhost';
                    process.env.PORT_NAME_PORT = '8080';
                    process.env.PORT_NAME_OTHER_CONFIG__MY_CONFIG = 'true'; -
                    - -
                    // Provider configuration
                    {
                    name: 'port-name',
                    type: 'http',
                    validation: {...},
                    envPrefix: 'PORT_NAME_',
                    } -
                    - -
                    // Resulting configuration
                    {
                    type: 'http',
                    host: 'localhost',
                    port: 8080,
                    otherConfig: {
                    myConfig: true,
                    },
                    } -
                    - -

                    Port validation options

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.APISource.html b/docs/interfaces/_mdf_js_crash.APISource.html deleted file mode 100644 index ba5b8e1b..00000000 --- a/docs/interfaces/_mdf_js_crash.APISource.html +++ /dev/null @@ -1,6 +0,0 @@ -APISource | @mdf.js

                    Object with the key information of the requested resource in the REST API context

                    -
                    interface APISource {
                        parameter: {
                            [x: string]: any;
                        };
                        pointer: string;
                    }

                    Properties

                    Properties

                    parameter: {
                        [x: string]: any;
                    }

                    A string indicating which URI query parameter caused the error

                    -
                    pointer: string

                    Pointer to the associated resource in the request [e.g."data/job/title"]

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.BoomOptions.html b/docs/interfaces/_mdf_js_crash.BoomOptions.html deleted file mode 100644 index 66452ba7..00000000 --- a/docs/interfaces/_mdf_js_crash.BoomOptions.html +++ /dev/null @@ -1,12 +0,0 @@ -BoomOptions | @mdf.js

                    Interface BoomOptions

                    Boom error configuration options

                    -
                    interface BoomOptions {
                        cause?: Cause;
                        info?: {
                            date?: Date;
                            subject?: string;
                            [x: string]: any;
                        };
                        links?: Links;
                        name?: string;
                        source?: APISource;
                    }

                    Hierarchy

                    • BaseOptions
                      • BoomOptions

                    Properties

                    Properties

                    cause?: Cause
                    info?: {
                        date?: Date;
                        subject?: string;
                        [x: string]: any;
                    }

                    Extra information error

                    -

                    Type declaration

                    • [x: string]: any

                      Any other relevant information

                      -
                    • Optionaldate?: Date

                      Date of the error

                      -
                    • Optionalsubject?: string

                      Subject to which the error relates

                      -
                    links?: Links
                    name?: string

                    Name of the error, used as a category

                    -
                    source?: APISource
                    diff --git a/docs/interfaces/_mdf_js_crash.CrashObject.html b/docs/interfaces/_mdf_js_crash.CrashObject.html deleted file mode 100644 index 814f9ea2..00000000 --- a/docs/interfaces/_mdf_js_crash.CrashObject.html +++ /dev/null @@ -1,16 +0,0 @@ -CrashObject | @mdf.js

                    Interface CrashObject

                    Crash error object output

                    -
                    interface CrashObject {
                        info?: Record<string, unknown>;
                        message: string;
                        name: string;
                        subject: string;
                        timestamp: string;
                        trace: string[];
                        uuid: string;
                    }

                    Hierarchy

                    • BaseObject
                      • CrashObject

                    Properties

                    info?: Record<string, unknown>

                    Extra error information

                    -
                    message: string

                    Human friendly error message

                    -
                    name: string

                    Name of the error

                    -
                    subject: string

                    Error subject

                    -
                    timestamp: string

                    Timestamp of the error

                    -
                    trace: string[]

                    Stack of error messages arranged according to the hierarchy of errors and causes

                    -
                    uuid: string

                    Identification of the process, request or transaction where the error appears

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.CrashOptions.html b/docs/interfaces/_mdf_js_crash.CrashOptions.html deleted file mode 100644 index 7f447145..00000000 --- a/docs/interfaces/_mdf_js_crash.CrashOptions.html +++ /dev/null @@ -1,11 +0,0 @@ -CrashOptions | @mdf.js

                    Interface CrashOptions

                    Crash error configuration options

                    -
                    interface CrashOptions {
                        cause?: Cause;
                        info?: {
                            date?: Date;
                            subject?: string;
                            [x: string]: any;
                        };
                        name?: string;
                    }

                    Hierarchy

                    • BaseOptions
                      • CrashOptions

                    Properties

                    Properties

                    cause?: Cause

                    Error that caused the creation of this instance

                    -
                    info?: {
                        date?: Date;
                        subject?: string;
                        [x: string]: any;
                    }

                    Extra information error

                    -

                    Type declaration

                    • [x: string]: any

                      Any other relevant information

                      -
                    • Optionaldate?: Date

                      Date of the error

                      -
                    • Optionalsubject?: string

                      Subject to which the error relates

                      -
                    name?: string

                    Name of the error, used as a category

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.MultiObject.html b/docs/interfaces/_mdf_js_crash.MultiObject.html deleted file mode 100644 index f5340e49..00000000 --- a/docs/interfaces/_mdf_js_crash.MultiObject.html +++ /dev/null @@ -1,16 +0,0 @@ -MultiObject | @mdf.js

                    Interface MultiObject

                    Multi error object output

                    -
                    interface MultiObject {
                        info?: Record<string, unknown>;
                        message: string;
                        name: string;
                        subject: string;
                        timestamp: string;
                        trace: string[];
                        uuid: string;
                    }

                    Hierarchy

                    • BaseObject
                      • MultiObject

                    Properties

                    info?: Record<string, unknown>

                    Extra error information

                    -
                    message: string

                    Human friendly error message

                    -
                    name: string

                    Name of the error

                    -
                    subject: string

                    Error subject

                    -
                    timestamp: string

                    Timestamp of the error

                    -
                    trace: string[]

                    Stack of error messages arranged according to the hierarchy of errors and causes

                    -
                    uuid: string

                    Identification of the process, request or transaction where the error appears

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.MultiOptions.html b/docs/interfaces/_mdf_js_crash.MultiOptions.html deleted file mode 100644 index 5b08681d..00000000 --- a/docs/interfaces/_mdf_js_crash.MultiOptions.html +++ /dev/null @@ -1,11 +0,0 @@ -MultiOptions | @mdf.js

                    Interface MultiOptions

                    Multi error configuration options

                    -
                    interface MultiOptions {
                        causes?: Error | Crash | (Error | Crash)[];
                        info?: {
                            date?: Date;
                            subject?: string;
                            [x: string]: any;
                        };
                        name?: string;
                    }

                    Hierarchy

                    • BaseOptions
                      • MultiOptions

                    Properties

                    Properties

                    causes?: Error | Crash | (Error | Crash)[]

                    Errors that caused the creation of this instance

                    -
                    info?: {
                        date?: Date;
                        subject?: string;
                        [x: string]: any;
                    }

                    Extra information error

                    -

                    Type declaration

                    • [x: string]: any

                      Any other relevant information

                      -
                    • Optionaldate?: Date

                      Date of the error

                      -
                    • Optionalsubject?: string

                      Subject to which the error relates

                      -
                    name?: string

                    Name of the error, used as a category

                    -
                    diff --git a/docs/interfaces/_mdf_js_crash.ValidationError.html b/docs/interfaces/_mdf_js_crash.ValidationError.html deleted file mode 100644 index 50bb3d7e..00000000 --- a/docs/interfaces/_mdf_js_crash.ValidationError.html +++ /dev/null @@ -1,12 +0,0 @@ -ValidationError | @mdf.js

                    Interface ValidationError

                    ValidationError interface from Joi library

                    -
                    interface ValidationError {
                        _original: any;
                        details: ValidationErrorItem[];
                        isJoi: boolean;
                        name: "ValidationError";
                        annotate(stripColors?: boolean): string;
                    }

                    Hierarchy

                    • Error
                      • ValidationError

                    Properties

                    Methods

                    Properties

                    _original: any

                    Array of errors

                    -
                    isJoi: boolean
                    name: "ValidationError"

                    Methods

                    • Function that returns a string with an annotated version of the object pointing at the places -where errors occurred. -NOTE: This method does not exist in browser builds of Joi

                      -

                      Parameters

                      • OptionalstripColors: boolean

                        if truthy, will strip the colors out of the output.

                        -

                      Returns string

                    diff --git a/docs/interfaces/_mdf_js_crash.ValidationErrorItem.html b/docs/interfaces/_mdf_js_crash.ValidationErrorItem.html deleted file mode 100644 index ba288cf8..00000000 --- a/docs/interfaces/_mdf_js_crash.ValidationErrorItem.html +++ /dev/null @@ -1,6 +0,0 @@ -ValidationErrorItem | @mdf.js

                    Interface ValidationErrorItem

                    ValidationErrorItem interface from Joi library

                    -
                    interface ValidationErrorItem {
                        context?: Context;
                        message: string;
                        path: (string | number)[];
                        type: string;
                    }

                    Properties

                    Properties

                    context?: Context
                    message: string
                    path: (string | number)[]
                    type: string
                    diff --git a/docs/interfaces/_mdf_js_doorkeeper.DoorkeeperOptions.html b/docs/interfaces/_mdf_js_doorkeeper.DoorkeeperOptions.html deleted file mode 100644 index b70a43a4..00000000 --- a/docs/interfaces/_mdf_js_doorkeeper.DoorkeeperOptions.html +++ /dev/null @@ -1,5 +0,0 @@ -DoorkeeperOptions | @mdf.js

                    This is the AJV Options object, but allErrors property is always true by default

                    -

                    See AJV Options for more information

                    -
                    interface DoorkeeperOptions {
                        dynamicDefaults?: Record<string, DynamicDefaultFunc>;
                    }

                    Hierarchy

                    • Omit<Options, "allErrors">
                      • DoorkeeperOptions

                    Properties

                    Properties

                    dynamicDefaults?: Record<string, DynamicDefaultFunc>

                    Dynamic defaults to be used in the schemas

                    -
                    diff --git a/docs/interfaces/_mdf_js_faker.DefaultOptions.html b/docs/interfaces/_mdf_js_faker.DefaultOptions.html deleted file mode 100644 index 9df5ca8f..00000000 --- a/docs/interfaces/_mdf_js_faker.DefaultOptions.html +++ /dev/null @@ -1,3 +0,0 @@ -DefaultOptions | @mdf.js

                    Interface DefaultOptions

                    Interface for default options

                    -
                    interface DefaultOptions {
                        likelihood?: number;
                        [key: string]: any;
                    }

                    Indexable

                    • [key: string]: any

                    Properties

                    Properties

                    likelihood?: number
                    diff --git a/docs/interfaces/_mdf_js_firehose.FirehoseOptions.html b/docs/interfaces/_mdf_js_firehose.FirehoseOptions.html deleted file mode 100644 index faa4f4e6..00000000 --- a/docs/interfaces/_mdf_js_firehose.FirehoseOptions.html +++ /dev/null @@ -1,19 +0,0 @@ -FirehoseOptions | @mdf.js

                    Interface FirehoseOptions<Type, Data, CustomHeaders, CustomOptions>

                    interface FirehoseOptions<Type, Data, CustomHeaders, CustomOptions> {
                        atLeastOne?: boolean;
                        bufferSize?: number;
                        logger?: LoggerInstance;
                        maxInactivityTime?: number;
                        postConsumeOptions?: PostConsumeOptions;
                        retryOptions?: RetryOptions;
                        sinks: Plugs.Sink.Any<Type, Data, CustomHeaders, CustomOptions>[];
                        sources: Plugs.Source.Any<Type, Data, CustomHeaders, CustomOptions>[];
                        strategies?: {
                            [type: string]: Jobs.Strategy<Type, Data, CustomHeaders, CustomOptions>[];
                        };
                    }

                    Type Parameters

                    • Type extends string = string
                    • Data = any
                    • CustomHeaders extends Record<string, any> = AnyHeaders
                    • CustomOptions extends Record<string, any> = AnyOptions

                    Properties

                    atLeastOne?: boolean

                    Define the number of sinks that must confirm a job, default options is all of them

                    -
                    bufferSize?: number

                    Buffer sizes

                    -

                    Logger instance for deep debugging tasks

                    -
                    maxInactivityTime?: number

                    Maximum time of inactivity before the firehose notify that is hold

                    -
                    postConsumeOptions?: PostConsumeOptions

                    Post consume operation options

                    -
                    retryOptions?: RetryOptions

                    Retry options for sink/source operations

                    -

                    Firehose sinks

                    -

                    Firehose sources

                    -
                    strategies?: {
                        [type: string]: Jobs.Strategy<Type, Data, CustomHeaders, CustomOptions>[];
                    }

                    Firehose transformation strategies per job type

                    -
                    diff --git a/docs/interfaces/_mdf_js_firehose.Plugs.Sink.Jet.html b/docs/interfaces/_mdf_js_firehose.Plugs.Sink.Jet.html deleted file mode 100644 index 87471560..00000000 --- a/docs/interfaces/_mdf_js_firehose.Plugs.Sink.Jet.html +++ /dev/null @@ -1,16 +0,0 @@ -Jet | @mdf.js

                    Interface Jet<Type, Data, CustomHeaders, CustomOptions>

                    interface Jet<Type, Data, CustomHeaders, CustomOptions> {
                        metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                        multi: ((jobs: JobObject<Type, Data, CustomHeaders, CustomOptions>[]) => Promise<void>);
                        single: ((job: JobObject<Type, Data, CustomHeaders, CustomOptions>) => Promise<void>);
                        on(event: "error", listener: ((error: Crash | Error) => void)): this;
                        on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                        start(): Promise<void>;
                        stop(): Promise<void>;
                    }

                    Type Parameters

                    • Type extends string = string
                    • Data = any
                    • CustomHeaders extends Record<string, any> = AnyHeaders
                    • CustomOptions extends Record<string, any> = AnyOptions

                    Hierarchy

                    Properties

                    Methods

                    Properties

                    metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                    Metrics registry for this component

                    -
                    multi: ((jobs: JobObject<Type, Data, CustomHeaders, CustomOptions>[]) => Promise<void>)

                    Perform the processing of several Jobs

                    -

                    Type declaration

                    single: ((job: JobObject<Type, Data, CustomHeaders, CustomOptions>) => Promise<void>)

                    Perform the processing of a single Job

                    -

                    Type declaration

                    Methods

                    • Emitted when the component throw an error

                      -

                      Parameters

                      • event: "error"
                      • listener: ((error: Crash | Error) => void)
                          • (error): void
                          • Parameters

                            Returns void

                      Returns this

                    • Emitted on every status change

                      -

                      Parameters

                      • event: "status"
                      • listener: ((status: "pass" | "fail" | "warn") => void)
                          • (status): void
                          • Parameters

                            • status: "pass" | "fail" | "warn"

                            Returns void

                      Returns this

                    • Start the Plug and the underlayer resources, making it available

                      -

                      Returns Promise<void>

                    • Stop the Plug and the underlayer resources, making it unavailable

                      -

                      Returns Promise<void>

                    diff --git a/docs/interfaces/_mdf_js_firehose.Plugs.Source.CreditsFlow.html b/docs/interfaces/_mdf_js_firehose.Plugs.Source.CreditsFlow.html deleted file mode 100644 index c0e3ffb8..00000000 --- a/docs/interfaces/_mdf_js_firehose.Plugs.Source.CreditsFlow.html +++ /dev/null @@ -1,21 +0,0 @@ -CreditsFlow | @mdf.js

                    Interface CreditsFlow<Type, Data, CustomHeaders, CustomOptions>

                    interface CreditsFlow<Type, Data, CustomHeaders, CustomOptions> {
                        metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                        postConsume: ((jobId: string) => Promise<undefined | string>);
                        addCredits(credits: number): Promise<number>;
                        on(event: "error", listener: ((error: Crash | Error) => void)): this;
                        on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                        on(event: "data", listener: ((job: JobRequest<Type, Data, CustomHeaders, CustomOptions>) => void)): this;
                        start(): Promise<void>;
                        stop(): Promise<void>;
                    }

                    Type Parameters

                    • Type extends string = string
                    • Data = any
                    • CustomHeaders extends Record<string, any> = AnyHeaders
                    • CustomOptions extends Record<string, any> = AnyOptions

                    Hierarchy

                    Properties

                    Methods

                    Properties

                    metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                    Metrics registry for this component

                    -
                    postConsume: ((jobId: string) => Promise<undefined | string>)

                    Perform the task to clean the job registers after the job has been resolved

                    -

                    Type declaration

                      • (jobId): Promise<undefined | string>
                      • Parameters

                        • jobId: string

                          Job entry identification

                          -

                        Returns Promise<undefined | string>

                          -
                        • the job entry identification that has been correctly removed or undefined if the job -was not found
                        • -
                        -

                    Methods

                    • Add new credits to the source

                      -

                      Parameters

                      • credits: number

                        Credits to be added to the source

                        -

                      Returns Promise<number>

                    • Emitted when the component throw an error

                      -

                      Parameters

                      • event: "error"
                      • listener: ((error: Crash | Error) => void)
                          • (error): void
                          • Parameters

                            Returns void

                      Returns this

                    • Emitted on every status change

                      -

                      Parameters

                      • event: "status"
                      • listener: ((status: "pass" | "fail" | "warn") => void)
                          • (status): void
                          • Parameters

                            • status: "pass" | "fail" | "warn"

                            Returns void

                      Returns this

                    • Emitted when there is a new job to be managed

                      -

                      Parameters

                      Returns this

                    • Start the Plug and the underlayer resources, making it available

                      -

                      Returns Promise<void>

                    • Stop the Plug and the underlayer resources, making it unavailable

                      -

                      Returns Promise<void>

                    diff --git a/docs/interfaces/_mdf_js_firehose.Plugs.Source.Flow.html b/docs/interfaces/_mdf_js_firehose.Plugs.Source.Flow.html deleted file mode 100644 index a32c76fc..00000000 --- a/docs/interfaces/_mdf_js_firehose.Plugs.Source.Flow.html +++ /dev/null @@ -1,22 +0,0 @@ -Flow | @mdf.js

                    Interface Flow<Type, Data, CustomHeaders, CustomOptions>

                    interface Flow<Type, Data, CustomHeaders, CustomOptions> {
                        metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                        postConsume: ((jobId: string) => Promise<undefined | string>);
                        init(): void;
                        on(event: "error", listener: ((error: Crash | Error) => void)): this;
                        on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                        on(event: "data", listener: ((job: JobRequest<Type, Data, CustomHeaders, CustomOptions>) => void)): this;
                        pause(): void;
                        start(): Promise<void>;
                        stop(): Promise<void>;
                    }

                    Type Parameters

                    • Type extends string = string
                    • Data = any
                    • CustomHeaders extends Record<string, any> = AnyHeaders
                    • CustomOptions extends Record<string, any> = AnyOptions

                    Hierarchy

                    Properties

                    Methods

                    Properties

                    metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                    Metrics registry for this component

                    -
                    postConsume: ((jobId: string) => Promise<undefined | string>)

                    Perform the task to clean the job registers after the job has been resolved

                    -

                    Type declaration

                      • (jobId): Promise<undefined | string>
                      • Parameters

                        • jobId: string

                          Job entry identification

                          -

                        Returns Promise<undefined | string>

                          -
                        • the job entry identification that has been correctly removed or undefined if the job -was not found
                        • -
                        -

                    Methods

                    • Enable consuming process

                      -

                      Returns void

                    • Emitted when the component throw an error

                      -

                      Parameters

                      • event: "error"
                      • listener: ((error: Crash | Error) => void)
                          • (error): void
                          • Parameters

                            Returns void

                      Returns this

                    • Emitted on every status change

                      -

                      Parameters

                      • event: "status"
                      • listener: ((status: "pass" | "fail" | "warn") => void)
                          • (status): void
                          • Parameters

                            • status: "pass" | "fail" | "warn"

                            Returns void

                      Returns this

                    • Emitted when there is a new job to be managed

                      -

                      Parameters

                      Returns this

                    • Stop consuming process

                      -

                      Returns void

                    • Start the Plug and the underlayer resources, making it available

                      -

                      Returns Promise<void>

                    • Stop the Plug and the underlayer resources, making it unavailable

                      -

                      Returns Promise<void>

                    diff --git a/docs/interfaces/_mdf_js_firehose.Plugs.Source.Sequence.html b/docs/interfaces/_mdf_js_firehose.Plugs.Source.Sequence.html deleted file mode 100644 index d170827a..00000000 --- a/docs/interfaces/_mdf_js_firehose.Plugs.Source.Sequence.html +++ /dev/null @@ -1,21 +0,0 @@ -Sequence | @mdf.js

                    Interface Sequence<Type, Data, CustomHeaders, CustomOptions>

                    interface Sequence<Type, Data, CustomHeaders, CustomOptions> {
                        metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">;
                        postConsume: ((jobId: string) => Promise<undefined | string>);
                        ingestData(size: number): Promise<JobRequest<Type, Data, CustomHeaders, CustomOptions> | JobRequest<Type, Data, CustomHeaders, CustomOptions>[]>;
                        on(event: "error", listener: ((error: Crash | Error) => void)): this;
                        on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                        on(event: "data", listener: ((job: JobRequest<Type, Data, CustomHeaders, CustomOptions>) => void)): this;
                        start(): Promise<void>;
                        stop(): Promise<void>;
                    }

                    Type Parameters

                    • Type extends string = string
                    • Data = any
                    • CustomHeaders extends Record<string, any> = AnyHeaders
                    • CustomOptions extends Record<string, any> = AnyOptions

                    Hierarchy

                    Properties

                    Methods

                    Properties

                    metrics?: Registry<"text/plain; version=0.0.4; charset=utf-8">

                    Metrics registry for this component

                    -
                    postConsume: ((jobId: string) => Promise<undefined | string>)

                    Perform the task to clean the job registers after the job has been resolved

                    -

                    Type declaration

                      • (jobId): Promise<undefined | string>
                      • Parameters

                        • jobId: string

                          Job entry identification

                          -

                        Returns Promise<undefined | string>

                          -
                        • the job entry identification that has been correctly removed or undefined if the job -was not found
                        • -
                        -

                    Methods

                    • Emitted when the component throw an error

                      -

                      Parameters

                      • event: "error"
                      • listener: ((error: Crash | Error) => void)
                          • (error): void
                          • Parameters

                            Returns void

                      Returns this

                    • Emitted on every status change

                      -

                      Parameters

                      • event: "status"
                      • listener: ((status: "pass" | "fail" | "warn") => void)
                          • (status): void
                          • Parameters

                            • status: "pass" | "fail" | "warn"

                            Returns void

                      Returns this

                    • Emitted when there is a new job to be managed

                      -

                      Parameters

                      Returns this

                    • Start the Plug and the underlayer resources, making it available

                      -

                      Returns Promise<void>

                    • Stop the Plug and the underlayer resources, making it unavailable

                      -

                      Returns Promise<void>

                    diff --git a/docs/interfaces/_mdf_js_firehose.PostConsumeOptions.html b/docs/interfaces/_mdf_js_firehose.PostConsumeOptions.html deleted file mode 100644 index 2e1ce2e9..00000000 --- a/docs/interfaces/_mdf_js_firehose.PostConsumeOptions.html +++ /dev/null @@ -1,8 +0,0 @@ -PostConsumeOptions | @mdf.js

                    Interface PostConsumeOptions

                    Copyright 2024 Mytra Control S.L. All rights reserved.

                    -

                    Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                    -
                    interface PostConsumeOptions {
                        checkUncleanedInterval?: number;
                        maxUnknownJobs?: number;
                    }

                    Properties

                    checkUncleanedInterval?: number

                    Time to wait between check buffer of uncleaned entries

                    -
                    maxUnknownJobs?: number

                    Number of unknown jobs to register

                    -
                    diff --git a/docs/interfaces/_mdf_js_http_client_provider.HTTP.Config.html b/docs/interfaces/_mdf_js_http_client_provider.HTTP.Config.html deleted file mode 100644 index 9a4f3ac0..00000000 --- a/docs/interfaces/_mdf_js_http_client_provider.HTTP.Config.html +++ /dev/null @@ -1,4 +0,0 @@ -Config | @mdf.js
                    interface Config {
                        httpAgentOptions?: AgentOptions;
                        httpsAgentOptions?: AgentOptions;
                        requestConfig?: CreateAxiosDefaults<any>;
                    }

                    Properties

                    httpAgentOptions?: AgentOptions
                    httpsAgentOptions?: AgentOptions
                    requestConfig?: CreateAxiosDefaults<any>
                    diff --git a/docs/interfaces/_mdf_js_http_server_provider.HTTP.Config.html b/docs/interfaces/_mdf_js_http_server_provider.HTTP.Config.html deleted file mode 100644 index 47fe130f..00000000 --- a/docs/interfaces/_mdf_js_http_server_provider.HTTP.Config.html +++ /dev/null @@ -1,4 +0,0 @@ -Config | @mdf.js
                    interface Config {
                        app?: Express;
                        host?: string;
                        port?: number;
                    }

                    Properties

                    Properties

                    app?: Express
                    host?: string
                    port?: number
                    diff --git a/docs/interfaces/_mdf_js_jsonl_archiver.ArchiveOptions.html b/docs/interfaces/_mdf_js_jsonl_archiver.ArchiveOptions.html deleted file mode 100644 index 0c926ace..00000000 --- a/docs/interfaces/_mdf_js_jsonl_archiver.ArchiveOptions.html +++ /dev/null @@ -1,129 +0,0 @@ -ArchiveOptions | @mdf.js

                    Represents the options for the JsonlFileStoreManager

                    -
                    interface ArchiveOptions {
                        archiveFolderPath: string;
                        createFolders: boolean;
                        defaultBaseFilename?: string;
                        fileEncoding: BufferEncoding;
                        inactiveTimeout?: number;
                        logger?: LoggerInstance;
                        propertyData?: string;
                        propertyFileName?: string;
                        propertySkip?: string;
                        propertySkipValue?: string | number | boolean;
                        retryOptions?: RetryOptions;
                        rotationInterval?: number;
                        rotationLines?: number;
                        rotationSize?: number;
                        separator?: string;
                        workingFolderPath: string;
                    }

                    Properties

                    archiveFolderPath: string

                    Path to the folder where the closed files are stored

                    -
                    './data/archive'
                    -
                    - -
                    './data/archive'
                    -
                    - -
                    createFolders: boolean

                    If true, it will create the folders if they don't exist

                    -
                    true
                    -
                    - -
                    true
                    -
                    - -
                    defaultBaseFilename?: string

                    Base filename for the files

                    -
                    'file'
                    -
                    - -
                    'file'
                    -
                    - -
                    fileEncoding: BufferEncoding

                    Encoding to use when writing to files

                    -
                    'utf-8'
                    -
                    - -
                    inactiveTimeout?: number

                    Maximum inactivity time in milliseconds before a handler is cleaned up

                    -
                    60000
                    -
                    - -
                    undefined
                    -
                    - -

                    Logger instance to use

                    -
                    undefined
                    -
                    - -
                    propertyData?: string

                    If set, this property will be used to store the data in the file, it could be a nested property -in the data object expressed as a dot separated string

                    -
                    'data.property'
                    -
                    - -
                    undefined
                    -
                    - -
                    propertyFileName?: string

                    If set, this property will be used as the filename, it could be a nested property in the data -object expressed as a dot separated string

                    -
                    'data.property'
                    -
                    - -
                    undefined
                    -
                    - -
                    propertySkip?: string

                    If set, this property will be used to skip the data, it could be a nested property in the data -object expressed as a dot separated string

                    -
                    'data.property'
                    -
                    - -
                    undefined
                    -
                    - -
                    propertySkipValue?: string | number | boolean

                    If set, this value will be used to skip the data, it could be a string, number or boolean. If -value is not set, but propertySkip is set, a not falsy value will be used to skip the data, -this means that any value that is not false, 0 or '' will be used to skip the data.

                    -
                    'skip' | 0 | false
                    -
                    - -
                    undefined
                    -
                    - -
                    retryOptions?: RetryOptions

                    Retry options for the file handler operations

                    -
                    { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                    -
                    - -
                    { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }
                    -
                    - -
                    rotationInterval?: number

                    Interval in milliseconds to rotate the file

                    -
                    3600000 (1 hour)
                    -
                    - -
                    undefined
                    -
                    - -
                    rotationLines?: number

                    Max number of lines before rotating the file

                    -
                    10000 (10k lines)
                    -
                    - -
                    undefined
                    -
                    - -
                    rotationSize?: number

                    Max size of the file before rotating it

                    -
                    10485760 (10 MB)
                    -
                    - -
                    undefined
                    -
                    - -
                    separator?: string

                    Separator to use when writing the data to the file

                    -
                    '\n'
                    -
                    - -
                    '\n'
                    -
                    - -
                    workingFolderPath: string

                    Path to the folder where the working files are stored

                    -
                    './data/working'
                    -
                    - -
                    './data/working'
                    -
                    - -
                    diff --git a/docs/interfaces/_mdf_js_jsonl_archiver.FileStats.html b/docs/interfaces/_mdf_js_jsonl_archiver.FileStats.html deleted file mode 100644 index 19822601..00000000 --- a/docs/interfaces/_mdf_js_jsonl_archiver.FileStats.html +++ /dev/null @@ -1,30 +0,0 @@ -FileStats | @mdf.js

                    Represents the statistics of a jsonl file

                    -
                    interface FileStats {
                        appendErrors: number;
                        appendSuccesses: number;
                        creationTimestamp: string;
                        currentSize: number;
                        fileName: string;
                        filePath: string;
                        handler: string;
                        isActive: boolean;
                        lastError?: Crash;
                        lastModifiedTimestamp: string;
                        lastRotationTimestamp: string;
                        numberLines: number;
                        onError: boolean;
                        rotationCount: number;
                    }

                    Properties

                    appendErrors: number

                    Number of append errors

                    -
                    appendSuccesses: number

                    Number of append successes in active file

                    -
                    creationTimestamp: string

                    Creation timestamp

                    -
                    currentSize: number

                    Current size in bytes

                    -
                    fileName: string

                    File name

                    -
                    filePath: string

                    File path

                    -
                    handler: string

                    File handler

                    -
                    isActive: boolean

                    Flag to indicate if the file is active

                    -
                    lastError?: Crash

                    Last error

                    -
                    lastModifiedTimestamp: string

                    Last modified timestamp

                    -
                    lastRotationTimestamp: string

                    Last rotation timestamp

                    -
                    numberLines: number

                    Number of lines

                    -
                    onError: boolean

                    Flag to indicate if the last operation was an error

                    -
                    rotationCount: number

                    Number of rotations

                    -
                    diff --git a/docs/interfaces/_mdf_js_kafka_provider.Consumer.Config.html b/docs/interfaces/_mdf_js_kafka_provider.Consumer.Config.html deleted file mode 100644 index 088d0c5a..00000000 --- a/docs/interfaces/_mdf_js_kafka_provider.Consumer.Config.html +++ /dev/null @@ -1,7 +0,0 @@ -Config | @mdf.js
                    interface Config {
                        client: KafkaConfig;
                        consumer: ConsumerConfig;
                        interval?: number;
                    }

                    Hierarchy

                    • BaseConfig
                      • Config

                    Properties

                    Properties

                    client: KafkaConfig

                    Kafka client configuration options

                    -
                    consumer: ConsumerConfig

                    Kafka consumer configuration options

                    -
                    interval?: number

                    Period of health check interval

                    -
                    diff --git a/docs/interfaces/_mdf_js_logger.LoggerInstance.html b/docs/interfaces/_mdf_js_logger.LoggerInstance.html deleted file mode 100644 index 25e7a241..00000000 --- a/docs/interfaces/_mdf_js_logger.LoggerInstance.html +++ /dev/null @@ -1,52 +0,0 @@ -LoggerInstance | @mdf.js

                    Interface LoggerInstance

                    Represents a logger instance with different log levels and functions.

                    -
                    interface LoggerInstance {
                        crash: ((error: Crash | Boom | Multi, context?: string) => void);
                        debug: LoggerFunction;
                        error: LoggerFunction;
                        info: LoggerFunction;
                        silly: LoggerFunction;
                        stream: {
                            write: ((message: string) => void);
                        };
                        verbose: LoggerFunction;
                        warn: LoggerFunction;
                    }

                    Implemented by

                    Properties

                    crash: ((error: Crash | Boom | Multi, context?: string) => void)

                    Log events in the ERROR level: all the information in a very detailed way. -This level used to be necessary only in the development process.

                    -

                    Type declaration

                      • (error, context?): void
                      • Parameters

                        • error: Crash | Boom | Multi

                          crash error instance

                          -
                        • Optionalcontext: string

                          context (class/function) where this logger is logging

                          -

                        Returns void

                    debug: LoggerFunction

                    Log events in the DEBUG level: all the information in a detailed way. -This level used to be necessary only in the debugging process, so not all the data is -reported, only the related with the main processes and tasks.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    error: LoggerFunction

                    Log events in the ERROR level: all the errors and problems with detailed information.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    info: LoggerFunction

                    Log events in the INFO level: only relevant events are reported. -This level is the default level.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    silly: LoggerFunction

                    Log events in the SILLY level: all the information in a very detailed way. -This level used to be necessary only in the development process, and the meta data used to be -the results of the operations.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    stream: {
                        write: ((message: string) => void);
                    }

                    Logs events in the stream.

                    -
                    verbose: LoggerFunction

                    Log events in the VERBOSE level: trace information without details. -This level used to be necessary only in system configuration process, so information about -the settings and startup process used to be reported.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    warn: LoggerFunction

                    Log events in the WARN level: information about possible problems or dangerous situations.

                    -

                    human readable information to log

                    -

                    unique identifier for the actual job/task/request process

                    -

                    context (class/function) where this logger is logging

                    -

                    extra information

                    -
                    diff --git a/docs/interfaces/_mdf_js_middlewares.CacheConfig.html b/docs/interfaces/_mdf_js_middlewares.CacheConfig.html deleted file mode 100644 index ea74d7dd..00000000 --- a/docs/interfaces/_mdf_js_middlewares.CacheConfig.html +++ /dev/null @@ -1,19 +0,0 @@ -CacheConfig | @mdf.js

                    Cache options interface

                    -
                    interface CacheConfig {
                        duration: number;
                        enabled: boolean;
                        headersBlacklist: string[];
                        prefixKey: string;
                        statusCodes: {
                            exclude: number[];
                            include: number[];
                        };
                        toggle: ((req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>) => boolean);
                        useBody: boolean;
                    }

                    Properties

                    duration: number

                    Default duration in seconds. Default: 10

                    -
                    enabled: boolean

                    Enablement flag. Default: true

                    -
                    headersBlacklist: string[]

                    List of header that should not be cached

                    -
                    prefixKey: string

                    Prefix key

                    -
                    statusCodes: {
                        exclude: number[];
                        include: number[];
                    }

                    List of status codes excluded and included

                    -

                    Type declaration

                    • exclude: number[]

                      Specifically Excluded status codes. Default: []

                      -
                    • include: number[]

                      Specifically Included status codes. Default: [200]

                      -
                    toggle: ((req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>) => boolean)

                    Toggle cache function. Takes the request/objects and must return a boolean value. If true, the -response will be cached

                    -
                    useBody: boolean

                    Use request has as part of cache key

                    -
                    diff --git a/docs/interfaces/_mdf_js_middlewares.CorsConfig.html b/docs/interfaces/_mdf_js_middlewares.CorsConfig.html deleted file mode 100644 index 06376ac0..00000000 --- a/docs/interfaces/_mdf_js_middlewares.CorsConfig.html +++ /dev/null @@ -1,14 +0,0 @@ -CorsConfig | @mdf.js

                    Copyright 2024 Mytra Control S.L. All rights reserved.

                    -

                    Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                    -
                    interface CorsConfig {
                        allowAppClients?: boolean;
                        allowHeaders?: string[];
                        credentials?: boolean;
                        enabled: boolean;
                        exposedHeaders?: string[];
                        maxAge?: number;
                        methods?: string[];
                        optionsSuccessStatus?: 200 | 204;
                        preflightContinue?: boolean;
                        whitelist?: string | (string | RegExp)[];
                    }

                    Properties

                    allowAppClients?: boolean
                    allowHeaders?: string[]
                    credentials?: boolean
                    enabled: boolean
                    exposedHeaders?: string[]
                    maxAge?: number
                    methods?: string[]
                    optionsSuccessStatus?: 200 | 204
                    preflightContinue?: boolean
                    whitelist?: string | (string | RegExp)[]
                    diff --git a/docs/interfaces/_mdf_js_middlewares.RateLimitConfig.html b/docs/interfaces/_mdf_js_middlewares.RateLimitConfig.html deleted file mode 100644 index b3af68c7..00000000 --- a/docs/interfaces/_mdf_js_middlewares.RateLimitConfig.html +++ /dev/null @@ -1,3 +0,0 @@ -RateLimitConfig | @mdf.js
                    interface RateLimitConfig {
                        enabled: boolean;
                        rates: RateLimitEntry[];
                    }

                    Properties

                    Properties

                    enabled: boolean
                    rates: RateLimitEntry[]
                    diff --git a/docs/interfaces/_mdf_js_mongo_provider.Mongo.Collections.html b/docs/interfaces/_mdf_js_mongo_provider.Mongo.Collections.html deleted file mode 100644 index f91c3169..00000000 --- a/docs/interfaces/_mdf_js_mongo_provider.Mongo.Collections.html +++ /dev/null @@ -1 +0,0 @@ -Collections | @mdf.js

                    Indexable

                    • [key: string]: {
                          indexes: IndexDescription[];
                          options: CreateCollectionOptions;
                      }
                      • indexes: IndexDescription[]
                      • options: CreateCollectionOptions
                    diff --git a/docs/interfaces/_mdf_js_mqtt_provider.MQTT.Config.html b/docs/interfaces/_mdf_js_mqtt_provider.MQTT.Config.html deleted file mode 100644 index d6ecfde6..00000000 --- a/docs/interfaces/_mdf_js_mqtt_provider.MQTT.Config.html +++ /dev/null @@ -1,3 +0,0 @@ -Config | @mdf.js
                    interface Config {
                        url?: string;
                    }

                    Hierarchy

                    • IClientOptions
                      • Config

                    Properties

                    Properties

                    url?: string

                    MQTT broker url

                    -
                    diff --git a/docs/interfaces/_mdf_js_openc2.ServiceBusOptions.html b/docs/interfaces/_mdf_js_openc2.ServiceBusOptions.html deleted file mode 100644 index aab81a72..00000000 --- a/docs/interfaces/_mdf_js_openc2.ServiceBusOptions.html +++ /dev/null @@ -1,5 +0,0 @@ -ServiceBusOptions | @mdf.js

                    Interface ServiceBusOptions

                    interface ServiceBusOptions {
                        secret?: string;
                        useJwt?: boolean;
                    }

                    Properties

                    Properties

                    secret?: string

                    Secret used in JWT token validation

                    -
                    useJwt?: boolean

                    Define the use of JWT tokens for client authentication

                    -
                    diff --git a/docs/interfaces/_mdf_js_openc2_core.ConsumerAdapter.html b/docs/interfaces/_mdf_js_openc2_core.ConsumerAdapter.html deleted file mode 100644 index 78702544..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.ConsumerAdapter.html +++ /dev/null @@ -1,14 +0,0 @@ -ConsumerAdapter | @mdf.js
                    interface ConsumerAdapter {
                        on(event: "error", listener: ((error: Crash | Error) => void)): this;
                        on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                        start(): Promise<void>;
                        stop(): Promise<void>;
                        subscribe(handler: OnCommandHandler): Promise<void>;
                        unsubscribe(handler: OnCommandHandler): Promise<void>;
                    }

                    Hierarchy

                    • ComponentAdapter
                      • ConsumerAdapter

                    Implemented by

                      Methods

                      • Emitted when a adapter's operation has some error

                        -

                        Parameters

                        • event: "error"
                        • listener: ((error: Crash | Error) => void)
                            • (error): void
                            • Parameters

                              Returns void

                        Returns this

                      • Emitted on every state change

                        -

                        Parameters

                        • event: "status"
                        • listener: ((status: "pass" | "fail" | "warn") => void)
                            • (status): void
                            • Parameters

                              • status: "pass" | "fail" | "warn"

                              Returns void

                        Returns this

                      • Connect the OpenC2 Adapter to the underlayer transport system

                        -

                        Returns Promise<void>

                      • Disconnect the OpenC2 Adapter to the underlayer transport system

                        -

                        Returns Promise<void>

                      • Subscribe the incoming command handler to the underlayer transport system

                        -

                        Parameters

                        Returns Promise<void>

                      • Unsubscribe the incoming command handler from the underlayer transport system

                        -

                        Parameters

                        Returns Promise<void>

                      diff --git a/docs/interfaces/_mdf_js_openc2_core.ConsumerOptions.html b/docs/interfaces/_mdf_js_openc2_core.ConsumerOptions.html deleted file mode 100644 index 0de9b733..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.ConsumerOptions.html +++ /dev/null @@ -1,21 +0,0 @@ -ConsumerOptions | @mdf.js
                      interface ConsumerOptions {
                          actionTargetPairs: ActionTargetPairs;
                          actuator?: string[];
                          id: string;
                          logger?: LoggerInstance;
                          maxInactivityTime?: number;
                          profiles?: string[];
                          registerLimit?: number;
                          registry?: Registry;
                          resolver?: ResolverMap;
                          retryOptions?: RetryOptions;
                      }

                      Hierarchy (view full)

                      Properties

                      actionTargetPairs: ActionTargetPairs

                      Supported pairs Action-Target pairs

                      -
                      actuator?: string[]

                      Actuator

                      -
                      id: string

                      Instance identification

                      -

                      Logger instance

                      -
                      maxInactivityTime?: number

                      Register max inactivity time

                      -
                      profiles?: string[]

                      Supported profiles

                      -
                      registerLimit?: number

                      Maximum number of message to be stored

                      -
                      registry?: Registry

                      Message and jobs registry

                      -
                      resolver?: ResolverMap

                      Resolver

                      -
                      retryOptions?: RetryOptions

                      Options for adapter retry operations

                      -
                      diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.Actuator.html b/docs/interfaces/_mdf_js_openc2_core.Control.Actuator.html deleted file mode 100644 index 19f10040..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.Control.Actuator.html +++ /dev/null @@ -1,2 +0,0 @@ -Actuator | @mdf.js

                      Domain based actuator identification

                      -

                      Indexable

                      • [domain: string]: Record<string, any>
                      diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.CommandMessage.html b/docs/interfaces/_mdf_js_openc2_core.Control.CommandMessage.html deleted file mode 100644 index 0b9e8930..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.Control.CommandMessage.html +++ /dev/null @@ -1,19 +0,0 @@ -CommandMessage | @mdf.js
                      interface CommandMessage {
                          content: Command;
                          content_type: string;
                          created: number;
                          from: string;
                          msg_type: Command;
                          request_id: string;
                          to: string[];
                      }

                      Hierarchy

                      • BaseMessage
                        • CommandMessage

                      Properties

                      content: Command

                      Message body as specified by content_type and msg_type

                      -
                      content_type: string

                      Media Type that identifies the format of the content, including major version. Incompatible -content formats must have different content_types. Content_type application/openc2 identifies -content defined by OpenC2 language specification versions 1.x, i.e., all versions that are -compatible with version 1.0.

                      -
                      created: number

                      Creation date/time of the content

                      -
                      from: string

                      Authenticated identifier of the creator of or authority for execution of a message

                      -
                      msg_type: Command

                      The type of command and control Message

                      -
                      request_id: string

                      A unique identifier created by the Producer and copied by Consumer into all Responses, in order -to support reference to a particular Command, transaction, or event chain

                      -
                      to: string[]

                      Authenticated identifier(s) of the authorized recipient(s) of a message

                      -
                      diff --git a/docs/interfaces/_mdf_js_openc2_core.Control.ResponseMessage.html b/docs/interfaces/_mdf_js_openc2_core.Control.ResponseMessage.html deleted file mode 100644 index 184abb48..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.Control.ResponseMessage.html +++ /dev/null @@ -1,21 +0,0 @@ -ResponseMessage | @mdf.js
                      interface ResponseMessage {
                          content: Response;
                          content_type: string;
                          created: number;
                          from: string;
                          msg_type: Response;
                          request_id: string;
                          status: StatusCode;
                          to: string[];
                      }

                      Hierarchy

                      • BaseMessage
                        • ResponseMessage

                      Properties

                      content: Response

                      Message body as specified by content_type and msg_type

                      -
                      content_type: string

                      Media Type that identifies the format of the content, including major version. Incompatible -content formats must have different content_types. Content_type application/openc2 identifies -content defined by OpenC2 language specification versions 1.x, i.e., all versions that are -compatible with version 1.0.

                      -
                      created: number

                      Creation date/time of the content

                      -
                      from: string

                      Authenticated identifier of the creator of or authority for execution of a message

                      -
                      msg_type: Response

                      The type of command and control Message

                      -
                      request_id: string

                      A unique identifier created by the Producer and copied by Consumer into all Responses, in order -to support reference to a particular Command, transaction, or event chain

                      -
                      status: StatusCode

                      Populated with a numeric status code in Response

                      -
                      to: string[]

                      Authenticated identifier(s) of the authorized recipient(s) of a message

                      -
                      diff --git a/docs/interfaces/_mdf_js_openc2_core.GatewayOptions.html b/docs/interfaces/_mdf_js_openc2_core.GatewayOptions.html deleted file mode 100644 index d8bc100a..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.GatewayOptions.html +++ /dev/null @@ -1,32 +0,0 @@ -GatewayOptions | @mdf.js
                      interface GatewayOptions {
                          actionTargetPairs: ActionTargetPairs;
                          actuator?: string[];
                          agingInterval?: number;
                          bypassLookupIntervalChecks?: boolean;
                          delay?: number;
                          id: string;
                          logger?: LoggerInstance;
                          lookupInterval?: number;
                          lookupTimeout?: number;
                          maxAge?: number;
                          maxInactivityTime?: number;
                          profiles?: string[];
                          registerLimit?: number;
                          registry?: Registry;
                          resolver?: ResolverMap;
                          retryOptions?: RetryOptions;
                      }

                      Hierarchy (view full)

                      Properties

                      actionTargetPairs: ActionTargetPairs

                      Supported pairs Action-Target pairs

                      -
                      actuator?: string[]

                      Actuator

                      -
                      agingInterval?: number
                      bypassLookupIntervalChecks?: boolean

                      Bypass lookup interval times checks

                      -
                      delay?: number

                      Gateway delay

                      -
                      id: string

                      Instance identification

                      -

                      Logger instance

                      -
                      lookupInterval?: number

                      Lookup interval in milliseconds

                      -
                      lookupTimeout?: number

                      Lookup timeout in milliseconds

                      -
                      maxAge?: number

                      Max allowed age in in milliseconds for a table entry

                      -
                      maxInactivityTime?: number

                      Register max inactivity time

                      -
                      profiles?: string[]

                      Supported profiles

                      -
                      registerLimit?: number

                      Maximum number of message to be stored

                      -
                      registry?: Registry

                      Message and jobs registry

                      -
                      resolver?: ResolverMap

                      Resolver

                      -
                      retryOptions?: RetryOptions

                      Options for adapter retry operations

                      -
                      diff --git a/docs/interfaces/_mdf_js_openc2_core.ProducerAdapter.html b/docs/interfaces/_mdf_js_openc2_core.ProducerAdapter.html deleted file mode 100644 index c0c96503..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.ProducerAdapter.html +++ /dev/null @@ -1,11 +0,0 @@ -ProducerAdapter | @mdf.js
                      interface ProducerAdapter {
                          on(event: "error", listener: ((error: Crash | Error) => void)): this;
                          on(event: "status", listener: ((status: "pass" | "fail" | "warn") => void)): this;
                          publish(message: CommandMessage): Promise<void | ResponseMessage | ResponseMessage[]>;
                          start(): Promise<void>;
                          stop(): Promise<void>;
                      }

                      Hierarchy

                      • ComponentAdapter
                        • ProducerAdapter

                      Implemented by

                        Methods

                        Methods

                        • Emitted when a adapter's operation has some error

                          -

                          Parameters

                          • event: "error"
                          • listener: ((error: Crash | Error) => void)
                              • (error): void
                              • Parameters

                                Returns void

                          Returns this

                        • Emitted on every state change

                          -

                          Parameters

                          • event: "status"
                          • listener: ((status: "pass" | "fail" | "warn") => void)
                              • (status): void
                              • Parameters

                                • status: "pass" | "fail" | "warn"

                                Returns void

                          Returns this

                        • Connect the OpenC2 Adapter to the underlayer transport system

                          -

                          Returns Promise<void>

                        • Disconnect the OpenC2 Adapter to the underlayer transport system

                          -

                          Returns Promise<void>

                        diff --git a/docs/interfaces/_mdf_js_openc2_core.ProducerOptions.html b/docs/interfaces/_mdf_js_openc2_core.ProducerOptions.html deleted file mode 100644 index 443833d2..00000000 --- a/docs/interfaces/_mdf_js_openc2_core.ProducerOptions.html +++ /dev/null @@ -1,20 +0,0 @@ -ProducerOptions | @mdf.js
                        interface ProducerOptions {
                            agingInterval?: number;
                            id: string;
                            logger?: LoggerInstance;
                            lookupInterval?: number;
                            lookupTimeout?: number;
                            maxAge?: number;
                            maxInactivityTime?: number;
                            registerLimit?: number;
                            registry?: Registry;
                            retryOptions?: RetryOptions;
                        }

                        Hierarchy (view full)

                        Properties

                        agingInterval?: number
                        id: string

                        Instance identification

                        -

                        Logger instance

                        -
                        lookupInterval?: number

                        Lookup interval in milliseconds

                        -
                        lookupTimeout?: number

                        Lookup timeout in milliseconds

                        -
                        maxAge?: number

                        Max allowed age in in milliseconds for a table entry

                        -
                        maxInactivityTime?: number

                        Register max inactivity time

                        -
                        registerLimit?: number

                        Maximum number of message to be stored

                        -
                        registry?: Registry

                        Message and jobs registry

                        -
                        retryOptions?: RetryOptions

                        Options for adapter retry operations

                        -
                        diff --git a/docs/interfaces/_mdf_js_service_registry.ObservabilityServiceOptions.html b/docs/interfaces/_mdf_js_service_registry.ObservabilityServiceOptions.html deleted file mode 100644 index c986f74a..00000000 --- a/docs/interfaces/_mdf_js_service_registry.ObservabilityServiceOptions.html +++ /dev/null @@ -1,18 +0,0 @@ -ObservabilityServiceOptions | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -
                        interface ObservabilityServiceOptions {
                            clusterUpdateInterval?: number;
                            host?: string;
                            includeStack?: boolean;
                            isCluster?: boolean;
                            maxSize?: number;
                            port?: number;
                            primaryPort?: number;
                        }

                        Properties

                        clusterUpdateInterval?: number

                        Cluster polling interval

                        -
                        host?: string

                        Host IP Addresses to be attached

                        -
                        includeStack?: boolean

                        Include stack trace in the error

                        -
                        isCluster?: boolean

                        Enable cluster mode

                        -
                        maxSize?: number

                        Max size of the registry

                        -
                        port?: number

                        Port to listen for incoming requests

                        -
                        primaryPort?: number

                        Primary port to listen for incoming requests on cluster mode

                        -
                        diff --git a/docs/interfaces/_mdf_js_service_registry.ServiceRegistryOptions.html b/docs/interfaces/_mdf_js_service_registry.ServiceRegistryOptions.html deleted file mode 100644 index ee187ddd..00000000 --- a/docs/interfaces/_mdf_js_service_registry.ServiceRegistryOptions.html +++ /dev/null @@ -1,43 +0,0 @@ -ServiceRegistryOptions | @mdf.js

                        Interface ServiceRegistryOptions<CustomSettings>

                        Deploying configuration options. This configuration is used to setup the application or -microservice to be deployed in a specific environment, we can configure:

                        -
                          -
                        • Application/microservice metadata information.
                        • -
                        • Remoto control interface (OpenC2 Consumer)
                        • -
                        • Observability service.
                        • -
                        • Logger configuration.
                        • -
                        • Retry options.
                        • -
                        -
                        interface ServiceRegistryOptions<CustomSettings> {
                            adapterOptions?: ConsumerAdapterOptions;
                            configLoaderOptions?: Partial<Setup.Config<CustomSettings>>;
                            consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>;
                            loggerOptions?: Partial<LoggerConfig>;
                            metadata?: Partial<Metadata>;
                            observabilityOptions?: Partial<ObservabilityServiceOptions>;
                            retryOptions?: Partial<Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">>;
                        }

                        Type Parameters

                        Hierarchy (view full)

                        Properties

                        adapterOptions?: ConsumerAdapterOptions

                        Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, -consumer and adapter options must be provided, in other case the consumer will start with -a Dummy adapter with no connection to any external service, so only HTTP commands over the -observability endpoints will be processed.

                        -
                        configLoaderOptions?: Partial<Setup.Config<CustomSettings>>

                        Configuration loader options. These options is used to load the configuration information -of the application that is been wrapped by the Application Wrapper. This configuration could be -loaded from files or environment variables, or even both.

                        -

                        To understand the configuration loader options, check the documentation of the package -@mdf.js/service-setup-provider.

                        -

                        Use different files for Application Wrapper configuration and for your own services to -avoid conflicts.

                        -
                        consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>

                        OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 -consumer, that is used to receive and process OpenC2 commands. The consumer option in the -BootstrapOptions should be enabled to start the consumer.

                        -
                        loggerOptions?: Partial<LoggerConfig>

                        Logger Options. If provided, a logger instance from the @mdf.js/logger package will be -created and used by the application in all the internal services of the Application Wrapper. -At the same time, the logger is exposed to the application to be used in the application -services. If this options is not provided, a Debug logger will be used internally, but it -will not be exposed to the application.

                        -
                        metadata?: Partial<Metadata>

                        Metadata information of the application or microservice. This information is used to identify -the application in the logs, metrics, and traces... and is shown in the service observability -endpoints.

                        -
                        observabilityOptions?: Partial<ObservabilityServiceOptions>

                        Observability instance options

                        -
                        retryOptions?: Partial<Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">>

                        Retry options. If provided, the application will use this options to retry to start the -services/resources registered in the Application Wrapped instance. If this options is not -provided, the application will not retry to start the services/resources.

                        -
                        diff --git a/docs/interfaces/_mdf_js_service_registry.ServiceRegistrySettings.html b/docs/interfaces/_mdf_js_service_registry.ServiceRegistrySettings.html deleted file mode 100644 index 7b65299e..00000000 --- a/docs/interfaces/_mdf_js_service_registry.ServiceRegistrySettings.html +++ /dev/null @@ -1,43 +0,0 @@ -ServiceRegistrySettings | @mdf.js

                        Interface ServiceRegistrySettings<CustomSettings>

                        Deploying configuration options. This configuration is used to setup the application or -microservice to be deployed in a specific environment, we can configure:

                        -
                          -
                        • Application/microservice metadata information.
                        • -
                        • Remoto control interface (OpenC2 Consumer)
                        • -
                        • Observability service.
                        • -
                        • Logger configuration.
                        • -
                        • Retry options.
                        • -
                        -

                        Type Parameters

                        Hierarchy (view full)

                        Properties

                        adapterOptions?: ConsumerAdapterOptions

                        Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, -consumer and adapter options must be provided, in other case the consumer will start with -a Dummy adapter with no connection to any external service, so only HTTP commands over the -observability endpoints will be processed.

                        -
                        configLoaderOptions: Setup.Config<CustomSettings>

                        Configuration loader options. These options is used to load the configuration information -of the application that is been wrapped by the Application Wrapper. This configuration could be -loaded from files or environment variables, or even both.

                        -

                        To understand the configuration loader options, check the documentation of the package -@mdf.js/service-setup-provider.

                        -

                        Use different files for Application Wrapper configuration and for your own services to -avoid conflicts.

                        -
                        consumerOptions?: ConsumerOptions

                        OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 -consumer, that is used to receive and process OpenC2 commands. The consumer option in the -BootstrapOptions should be enabled to start the consumer.

                        -
                        loggerOptions: LoggerConfig

                        Logger Options. If provided, a logger instance from the @mdf.js/logger package will be -created and used by the application in all the internal services of the Application Wrapper. -At the same time, the logger is exposed to the application to be used in the application -services. If this options is not provided, a Debug logger will be used internally, but it -will not be exposed to the application.

                        -
                        metadata: Metadata

                        Metadata information of the application or microservice. This information is used to identify -the application in the logs, metrics, and traces... and is shown in the service observability -endpoints.

                        -
                        observabilityOptions: ObservabilityServiceOptions

                        Observability instance options

                        -
                        retryOptions: RetryOptions

                        Retry options. If provided, the application will use this options to retry to start the -services/resources registered in the Application Wrapped instance. If this options is not -provided, the application will not retry to start the services/resources.

                        -
                        diff --git a/docs/interfaces/_mdf_js_service_registry.ServiceSetting.html b/docs/interfaces/_mdf_js_service_registry.ServiceSetting.html deleted file mode 100644 index 9e6aaaf3..00000000 --- a/docs/interfaces/_mdf_js_service_registry.ServiceSetting.html +++ /dev/null @@ -1,38 +0,0 @@ -ServiceSetting | @mdf.js

                        Interface ServiceSetting<CustomSettings>

                        Service setting interface -Merge in the object the service registry settings and the custom settings.

                        -
                        interface ServiceSetting<CustomSettings> {
                            adapterOptions?: ConsumerAdapterOptions;
                            configLoaderOptions?: Partial<Setup.Config<Record<string, any>>>;
                            consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>;
                            custom: CustomSettings;
                            loggerOptions?: Partial<LoggerConfig>;
                            metadata?: Partial<Metadata>;
                            observabilityOptions?: Partial<ObservabilityServiceOptions>;
                            retryOptions?: Partial<Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">>;
                        }

                        Type Parameters

                        Hierarchy (view full)

                        Properties

                        adapterOptions?: ConsumerAdapterOptions

                        Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, -consumer and adapter options must be provided, in other case the consumer will start with -a Dummy adapter with no connection to any external service, so only HTTP commands over the -observability endpoints will be processed.

                        -
                        configLoaderOptions?: Partial<Setup.Config<Record<string, any>>>

                        Configuration loader options. These options is used to load the configuration information -of the application that is been wrapped by the Application Wrapper. This configuration could be -loaded from files or environment variables, or even both.

                        -

                        To understand the configuration loader options, check the documentation of the package -@mdf.js/service-setup-provider.

                        -

                        Use different files for Application Wrapper configuration and for your own services to -avoid conflicts.

                        -
                        consumerOptions?: Partial<Omit<ConsumerOptions, "logger" | "registry">>

                        OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 -consumer, that is used to receive and process OpenC2 commands. The consumer option in the -BootstrapOptions should be enabled to start the consumer.

                        -

                        Custom settings

                        -
                        loggerOptions?: Partial<LoggerConfig>

                        Logger Options. If provided, a logger instance from the @mdf.js/logger package will be -created and used by the application in all the internal services of the Application Wrapper. -At the same time, the logger is exposed to the application to be used in the application -services. If this options is not provided, a Debug logger will be used internally, but it -will not be exposed to the application.

                        -
                        metadata?: Partial<Metadata>

                        Metadata information of the application or microservice. This information is used to identify -the application in the logs, metrics, and traces... and is shown in the service observability -endpoints.

                        -
                        observabilityOptions?: Partial<ObservabilityServiceOptions>

                        Observability instance options

                        -
                        retryOptions?: Partial<Omit<RetryOptions, "logger" | "interrupt" | "abortSignal">>

                        Retry options. If provided, the application will use this options to retry to start the -services/resources registered in the Application Wrapped instance. If this options is not -provided, the application will not retry to start the services/resources.

                        -
                        diff --git a/docs/interfaces/_mdf_js_service_setup_provider.Setup.Config.html b/docs/interfaces/_mdf_js_service_setup_provider.Setup.Config.html deleted file mode 100644 index ed845a04..00000000 --- a/docs/interfaces/_mdf_js_service_setup_provider.Setup.Config.html +++ /dev/null @@ -1,51 +0,0 @@ -Config | @mdf.js
                        interface Config<SystemConfig> {
                            base?: Partial<SystemConfig>;
                            checker?: DoorKeeper<void>;
                            configFiles?: string[];
                            default?: Partial<SystemConfig>;
                            envPrefix?: string | string[] | Record<string, string>;
                            preset?: string;
                            presetFiles?: string[];
                            schema?: string;
                            schemaFiles?: string[];
                        }

                        Type Parameters

                        • SystemConfig extends Record<string, any> = Record<string, any>

                        Properties

                        base?: Partial<SystemConfig>

                        Object to be used as base and main configuration options. The configuration will be merged with -the configuration from the configuration files. This object will override the configuration -from the configuration files and the environment variables. The main reason of this option is -to allow the user to define some configuration in the code and let the rest of the -configuration to be loaded, using the Configuration Manager as unique source of configuration.

                        -
                        checker?: DoorKeeper<void>

                        DoorKeeper instance to be used to validate the configuration. If none is indicated, the setup -instance will be try to create a new DoorKeeper instance using the schema files indicated in -the options. If the schema files are not indicated, the configuration will not be validated.

                        -
                        configFiles?: string[]

                        List of configuration files to be loaded. The entries could be a file path or glob pattern. -All the files will be loaded and merged in the order they are founded. The result of the merge -will be used as the final configuration.

                        -
                        default?: Partial<SystemConfig>

                        Object to be used as default configuration options. The configuration will be merged with the -configuration from the configuration files, the environment variables and the base option. This -object will be used as the default configuration if no other configuration is found.

                        -
                        envPrefix?: string | string[] | Record<string, string>

                        Prefix or prefixes to use on configuration loading from the environment variables. The prefix -will be used to filter the environment variables. The prefix will be removed from the -environment variable name and the remaining part will be used as the configuration property -name. The configuration property name will be converted to camel case. -Environment variables will override the configuration from the configuration files.

                        -
                        `MY_APP_` // as single prefix
                        ['MY_APP_', 'MY_OTHER_APP_'] // as array of prefixes
                        { MY_APP: 'myApp', MY_OTHER_APP: 'myOtherApp' } // as object with prefixes -
                        - -
                        preset?: string

                        Preset to be used as configuration base, if none is indicated, or the indicated preset is -not found, the configuration from the configuration files will be used.

                        -
                        presetFiles?: string[]

                        List of files with preset options to be loaded. The entries could be a file path or glob -pattern. The first part of the file name will be used as the preset name. The file name -should be in the format of presetName.config.json or presetName.config.yaml. The name of -the preset will be used to merge different files in order to create a single preset.

                        -
                        `['./config/presets/*.json']`
                        -
                        - -
                        `['./config/presets/*.json', './config/presets/*.yaml']`
                        -
                        - -
                        `['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml']`
                        -
                        - -
                        schema?: string

                        Schema to be used to validate the configuration. If none is indicated, the configuration will -not be validated. The schema name should be the same as the file name without the extension.

                        -
                        schemaFiles?: string[]

                        List of files with JSON schemas used to validate the configuration. The entries could be a -file path or glob pattern.

                        -
                        diff --git a/docs/interfaces/_mdf_js_socket_client_provider.SocketIOClient.Config.html b/docs/interfaces/_mdf_js_socket_client_provider.SocketIOClient.Config.html deleted file mode 100644 index a6a35306..00000000 --- a/docs/interfaces/_mdf_js_socket_client_provider.SocketIOClient.Config.html +++ /dev/null @@ -1,3 +0,0 @@ -Config | @mdf.js
                        interface Config {
                            url?: string;
                        }

                        Hierarchy

                        • Partial<ManagerOptions>
                        • Partial<SocketOptions>
                          • Config

                        Properties

                        Properties

                        url?: string

                        URL to connect to the server

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.GroupTaskBaseConfig.html b/docs/interfaces/_mdf_js_tasks.GroupTaskBaseConfig.html deleted file mode 100644 index c199b2c2..00000000 --- a/docs/interfaces/_mdf_js_tasks.GroupTaskBaseConfig.html +++ /dev/null @@ -1,6 +0,0 @@ -GroupTaskBaseConfig | @mdf.js

                        Interface GroupTaskBaseConfig<Result, Binding>

                        Represents the base configuration for a group of tasks

                        -
                        interface GroupTaskBaseConfig<Result, Binding> {
                            options: WellIdentifiedTaskOptions<any>;
                            tasks: SingleTaskBaseConfig<Result, Binding>[];
                        }

                        Type Parameters

                        • Result = any
                        • Binding = any

                        Properties

                        Properties

                        Group of tasks options

                        -

                        Tasks

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.LimiterOptions.html b/docs/interfaces/_mdf_js_tasks.LimiterOptions.html deleted file mode 100644 index 13af79c9..00000000 --- a/docs/interfaces/_mdf_js_tasks.LimiterOptions.html +++ /dev/null @@ -1,58 +0,0 @@ -LimiterOptions | @mdf.js

                        Interface LimiterOptions

                        Represents the limiter options

                        -
                        interface LimiterOptions {
                            autoStart?: boolean;
                            bucketSize?: number;
                            concurrency?: number;
                            delay?: number;
                            highWater?: number;
                            interval?: number;
                            penalty?: number;
                            retryOptions?: RetryOptions;
                            strategy?: Strategy;
                            tokensPerInterval?: number;
                        }

                        Hierarchy (view full)

                        Properties

                        autoStart?: boolean

                        Set whether the limiter should start to process the jobs automatically

                        -
                        true
                        -
                        - -
                        bucketSize?: number

                        Set the bucket size for the rate limiter

                        -

                        0 -If the bucket size is 0, only concurrency and delay will be used to limit the rate of the -jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to -limit the rate of the jobs. The bucket size is the maximum number of tokens that can be -consumed in the interval. The interval is defined by the tokensPerInterval and interval -properties.

                        -

                        https://en.wikipedia.org/wiki/Token_bucket

                        -
                        concurrency?: number

                        The maximum number of concurrent jobs

                        -
                        1
                        -
                        - -
                        delay?: number

                        Delay between each job in milliseconds

                        -

                        0 -For concurrency = 1, the delay is applied after each job is finished -For concurrency > 1, if the actual number of concurrent jobs is less than concurrency, the -delay is applied after each job is finished, otherwise, the delay is applied after each job is -started.

                        -
                        highWater?: number

                        The maximum number of jobs in the queue

                        -
                        Infinity
                        -
                        - -
                        interval?: number

                        Define the interval in milliseconds

                        -
                        1000
                        -
                        - -
                        penalty?: number

                        The penalty for the BLOCK strategy in milliseconds

                        -
                        0
                        -
                        - -
                        retryOptions?: RetryOptions

                        Set the default options for the retry process of the jobs

                        -
                        undefined
                        -
                        - -
                        strategy?: Strategy

                        The strategy to use when the queue length reaches highWater

                        -
                        'leak'
                        -
                        - -
                        tokensPerInterval?: number

                        Define the number of tokens that will be added to the bucket at the beginning of the interval

                        -
                        1
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_tasks.MetaData.html b/docs/interfaces/_mdf_js_tasks.MetaData.html deleted file mode 100644 index ba008717..00000000 --- a/docs/interfaces/_mdf_js_tasks.MetaData.html +++ /dev/null @@ -1,29 +0,0 @@ -MetaData | @mdf.js

                        Metadata of the execution of the task

                        -
                        interface MetaData {
                            $meta?: MetaData[];
                            cancelledAt?: string;
                            completedAt?: string;
                            createdAt: string;
                            duration?: number;
                            executedAt?: string;
                            failedAt?: string;
                            priority: number;
                            reason?: string;
                            status: TaskState;
                            taskId: string;
                            uuid: string;
                            weight: number;
                        }

                        Properties

                        $meta?: MetaData[]

                        Additional metadata objects, store the metadata information from related tasks in a sequence or -group

                        -
                        cancelledAt?: string

                        Date when the task was cancelled in ISO format

                        -
                        completedAt?: string

                        Date when the task was completed in ISO format

                        -
                        createdAt: string

                        Date when the task was created

                        -
                        duration?: number

                        Duration of the task in milliseconds

                        -
                        executedAt?: string

                        Date when the task was executed in ISO format

                        -
                        failedAt?: string

                        Date when the task was failed in ISO format

                        -
                        priority: number

                        Task priority

                        -
                        reason?: string

                        Reason of failure or cancellation

                        -
                        status: TaskState

                        Status of the task

                        -
                        taskId: string

                        Task identifier, defined by the user

                        -
                        uuid: string

                        Unique task identification, unique for each task

                        -
                        weight: number

                        Task weight

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.PollingManagerOptions.html b/docs/interfaces/_mdf_js_tasks.PollingManagerOptions.html deleted file mode 100644 index 7bb87794..00000000 --- a/docs/interfaces/_mdf_js_tasks.PollingManagerOptions.html +++ /dev/null @@ -1,21 +0,0 @@ -PollingManagerOptions | @mdf.js

                        Interface PollingManagerOptions

                        interface PollingManagerOptions {
                            componentId: string;
                            cyclesOnStats?: number;
                            entries: TaskBaseConfig[];
                            logger?: LoggerInstance;
                            pollingGroup: PollingGroup;
                            resource: string;
                            slowCycleRatio?: number;
                        }

                        Properties

                        componentId: string

                        Component identifier

                        -
                        cyclesOnStats?: number

                        Number of cycles on stats

                        -
                        10
                        -
                        - -
                        entries: TaskBaseConfig[]

                        Tasks configuration

                        -

                        Logger instance

                        -
                        pollingGroup: PollingGroup

                        Polling group assigned to this manager

                        -
                        resource: string

                        Resource identifier

                        -
                        slowCycleRatio?: number

                        Number of fast cycles to run per slow cycle

                        -
                        3
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_tasks.PollingStats.html b/docs/interfaces/_mdf_js_tasks.PollingStats.html deleted file mode 100644 index dbaf7d59..00000000 --- a/docs/interfaces/_mdf_js_tasks.PollingStats.html +++ /dev/null @@ -1,28 +0,0 @@ -PollingStats | @mdf.js

                        Interface PollingStats

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -
                        interface PollingStats {
                            averageCycleDuration: number;
                            consecutiveOverruns: number;
                            cycles: number;
                            inFastCycleTasks: number;
                            inOffCycleTasks: number;
                            inSlowCycleTasks: number;
                            lastCycleDuration: number;
                            maxCycleDuration: number;
                            minCycleDuration: number;
                            overruns: number;
                            pendingTasks: number;
                            scanTime: Date;
                        }

                        Properties

                        averageCycleDuration: number

                        Average cycle duration in milliseconds

                        -
                        consecutiveOverruns: number

                        Number of consecutive overruns

                        -
                        cycles: number

                        Number of cycles performed

                        -
                        inFastCycleTasks: number

                        Number of tasks included on the fast cycle (normal cycle)

                        -
                        inOffCycleTasks: number

                        Number of tasks not included on the cycle

                        -
                        inSlowCycleTasks: number

                        Number of tasks included on the slow cycle

                        -
                        lastCycleDuration: number

                        Last cycle duration in milliseconds

                        -
                        maxCycleDuration: number

                        Maximum cycle duration in milliseconds

                        -
                        minCycleDuration: number

                        Minimum cycle duration in milliseconds

                        -
                        overruns: number

                        Number of cycles with overruns

                        -
                        pendingTasks: number

                        Number of pending tasks

                        -
                        scanTime: Date

                        Scan time

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.QueueOptions.html b/docs/interfaces/_mdf_js_tasks.QueueOptions.html deleted file mode 100644 index 73beb1f1..00000000 --- a/docs/interfaces/_mdf_js_tasks.QueueOptions.html +++ /dev/null @@ -1,36 +0,0 @@ -QueueOptions | @mdf.js

                        Interface QueueOptions

                        Represents the queue options

                        -
                        interface QueueOptions {
                            bucketSize?: number;
                            highWater?: number;
                            interval?: number;
                            penalty?: number;
                            strategy?: Strategy;
                            tokensPerInterval?: number;
                        }

                        Hierarchy (view full)

                        Properties

                        bucketSize?: number

                        Set the bucket size for the rate limiter

                        -

                        0 -If the bucket size is 0, only concurrency and delay will be used to limit the rate of the -jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to -limit the rate of the jobs. The bucket size is the maximum number of tokens that can be -consumed in the interval. The interval is defined by the tokensPerInterval and interval -properties.

                        -

                        https://en.wikipedia.org/wiki/Token_bucket

                        -
                        highWater?: number

                        The maximum number of jobs in the queue

                        -
                        Infinity
                        -
                        - -
                        interval?: number

                        Define the interval in milliseconds

                        -
                        1000
                        -
                        - -
                        penalty?: number

                        The penalty for the BLOCK strategy in milliseconds

                        -
                        0
                        -
                        - -
                        strategy?: Strategy

                        The strategy to use when the queue length reaches highWater

                        -
                        'leak'
                        -
                        - -
                        tokensPerInterval?: number

                        Define the number of tokens that will be added to the bucket at the beginning of the interval

                        -
                        1
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_tasks.ResourceConfigEntry.html b/docs/interfaces/_mdf_js_tasks.ResourceConfigEntry.html deleted file mode 100644 index 2b5e8edd..00000000 --- a/docs/interfaces/_mdf_js_tasks.ResourceConfigEntry.html +++ /dev/null @@ -1,6 +0,0 @@ -ResourceConfigEntry | @mdf.js

                        Interface ResourceConfigEntry<Result, Binding, PollingGroups>

                        Represents the resource configuration

                        -
                        interface ResourceConfigEntry<Result, Binding, PollingGroups> {
                            limiterOptions?: LimiterOptions;
                            pollingGroups: {
                                [polling in PollingGroup]?: TaskBaseConfig<Result, Binding>[]
                            };
                        }

                        Type Parameters

                        Properties

                        limiterOptions?: LimiterOptions

                        The limiter options

                        -
                        pollingGroups: {
                            [polling in PollingGroup]?: TaskBaseConfig<Result, Binding>[]
                        }

                        The polling groups

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.ResourcesConfigObject.html b/docs/interfaces/_mdf_js_tasks.ResourcesConfigObject.html deleted file mode 100644 index 1dbd3f73..00000000 --- a/docs/interfaces/_mdf_js_tasks.ResourcesConfigObject.html +++ /dev/null @@ -1,4 +0,0 @@ -ResourcesConfigObject | @mdf.js

                        Interface ResourcesConfigObject<Result, Binding, PollingGroups>

                        Represents the resources object, a map of resources with their polling groups and the tasks to -execute in that polling groups

                        -

                        Type Parameters

                        Indexable

                        diff --git a/docs/interfaces/_mdf_js_tasks.SchedulerOptions.html b/docs/interfaces/_mdf_js_tasks.SchedulerOptions.html deleted file mode 100644 index 59266f53..00000000 --- a/docs/interfaces/_mdf_js_tasks.SchedulerOptions.html +++ /dev/null @@ -1,18 +0,0 @@ -SchedulerOptions | @mdf.js

                        Interface SchedulerOptions<Result, Binding, PollingGroups>

                        Represents the options for the scheduler

                        -
                        interface SchedulerOptions<Result, Binding, PollingGroups> {
                            cyclesOnStats?: number;
                            limiterOptions?: LimiterOptions;
                            logger?: LoggerInstance;
                            resources?: ResourcesConfigObject<Result, Binding, PollingGroups>;
                            slowCycleRatio?: number;
                        }

                        Type Parameters

                        Properties

                        cyclesOnStats?: number

                        Number of cycles on stats

                        -
                        10
                        -
                        - -
                        limiterOptions?: LimiterOptions

                        The limiter options

                        -

                        The logger for the scheduler

                        -

                        The entries for the scheduler

                        -
                        slowCycleRatio?: number

                        Number of fast cycles to run per slow cycle

                        -
                        3
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_tasks.SequenceTaskBaseConfig.html b/docs/interfaces/_mdf_js_tasks.SequenceTaskBaseConfig.html deleted file mode 100644 index cdb6f608..00000000 --- a/docs/interfaces/_mdf_js_tasks.SequenceTaskBaseConfig.html +++ /dev/null @@ -1,6 +0,0 @@ -SequenceTaskBaseConfig | @mdf.js

                        Interface SequenceTaskBaseConfig<Result, Binding>

                        Represents the base configuration for a sequence of tasks

                        -
                        interface SequenceTaskBaseConfig<Result, Binding> {
                            options: WellIdentifiedTaskOptions<any>;
                            pattern: {
                                finally?: SingleTaskBaseConfig<Result, Binding>[];
                                post?: SingleTaskBaseConfig<Result, Binding>[];
                                pre?: SingleTaskBaseConfig<Result, Binding>[];
                                task: SingleTaskBaseConfig<Result, Binding>;
                            };
                        }

                        Type Parameters

                        • Result = any
                        • Binding = any

                        Properties

                        Properties

                        The schedule of the task

                        -
                        pattern: {
                            finally?: SingleTaskBaseConfig<Result, Binding>[];
                            post?: SingleTaskBaseConfig<Result, Binding>[];
                            pre?: SingleTaskBaseConfig<Result, Binding>[];
                            task: SingleTaskBaseConfig<Result, Binding>;
                        }

                        Task pattern

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.SingleTaskBaseConfig.html b/docs/interfaces/_mdf_js_tasks.SingleTaskBaseConfig.html deleted file mode 100644 index a1c679ab..00000000 --- a/docs/interfaces/_mdf_js_tasks.SingleTaskBaseConfig.html +++ /dev/null @@ -1,8 +0,0 @@ -SingleTaskBaseConfig | @mdf.js

                        Interface SingleTaskBaseConfig<Result, Binding>

                        Represents the base configuration for a single task

                        -
                        interface SingleTaskBaseConfig<Result, Binding> {
                            options: WellIdentifiedTaskOptions<Binding>;
                            task: TaskAsPromise<Result>;
                            taskArgs?: TaskArguments;
                        }

                        Type Parameters

                        • Result = any
                        • Binding = any

                        Properties

                        Properties

                        Task options

                        -

                        Task

                        -
                        taskArgs?: TaskArguments

                        Task arguments

                        -
                        diff --git a/docs/interfaces/_mdf_js_tasks.TaskOptions.html b/docs/interfaces/_mdf_js_tasks.TaskOptions.html deleted file mode 100644 index 2aa1783d..00000000 --- a/docs/interfaces/_mdf_js_tasks.TaskOptions.html +++ /dev/null @@ -1,32 +0,0 @@ -TaskOptions | @mdf.js

                        Interface TaskOptions<U>

                        Represents the options for a task

                        -
                        interface TaskOptions<U> {
                            bind?: U;
                            id?: string;
                            priority?: number;
                            retryOptions?: RetryOptions;
                            retryStrategy?: RetryStrategy;
                            weight?: number;
                        }

                        Type Parameters

                        • U

                        Hierarchy (view full)

                        Properties

                        bind?: U

                        Context to be bind to the task

                        -
                        id?: string

                        Task identifier, it necessary to identify the task during all the process, for example, when -the job is executed, the event with the task identifier will be emitted with the result of the -task.

                        -
                        If not provided, the task identifier will be generated automatically.
                        -
                        - -
                        priority?: number

                        The priority of the task. A higher value means a higher priority. The default priority is 0.

                        -
                        0
                        -
                        - -
                        retryOptions?: RetryOptions

                        Set the options for the retry process of the task

                        -
                        undefined
                        -
                        - -
                        retryStrategy?: RetryStrategy

                        Set the strategy to retry the task

                        -
                        RETRY_STRATEGY.RETRY
                        -
                        - -
                        weight?: number

                        The weight of the task, this define the number of tokens that the task will consume from the -bucket. The default weight is 1.

                        -
                        1
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_tasks.WellIdentifiedTaskOptions.html b/docs/interfaces/_mdf_js_tasks.WellIdentifiedTaskOptions.html deleted file mode 100644 index 46f35a21..00000000 --- a/docs/interfaces/_mdf_js_tasks.WellIdentifiedTaskOptions.html +++ /dev/null @@ -1,29 +0,0 @@ -WellIdentifiedTaskOptions | @mdf.js

                        Interface WellIdentifiedTaskOptions<Binding>

                        Extends the task options with the task identifier, making it mandatory

                        -
                        interface WellIdentifiedTaskOptions<Binding> {
                            bind?: Binding;
                            id: string;
                            priority?: number;
                            retryOptions?: RetryOptions;
                            retryStrategy?: RetryStrategy;
                            weight?: number;
                        }

                        Type Parameters

                        • Binding = any

                        Hierarchy (view full)

                        Properties

                        bind?: Binding

                        Context to be bind to the task

                        -
                        id: string

                        Task identifier, it necessary to identify the task during all the process, for example, when -the job is executed, the event with the task identifier will be emitted with the result of the -task.

                        -
                        priority?: number

                        The priority of the task. A higher value means a higher priority. The default priority is 0.

                        -
                        0
                        -
                        - -
                        retryOptions?: RetryOptions

                        Set the options for the retry process of the task

                        -
                        undefined
                        -
                        - -
                        retryStrategy?: RetryStrategy

                        Set the strategy to retry the task

                        -
                        RETRY_STRATEGY.RETRY
                        -
                        - -
                        weight?: number

                        The weight of the task, this define the number of tokens that the task will consume from the -bucket. The default weight is 1.

                        -
                        1
                        -
                        - -
                        diff --git a/docs/interfaces/_mdf_js_utils.RetryOptions.html b/docs/interfaces/_mdf_js_utils.RetryOptions.html deleted file mode 100644 index 0ad7f2f5..00000000 --- a/docs/interfaces/_mdf_js_utils.RetryOptions.html +++ /dev/null @@ -1,18 +0,0 @@ -RetryOptions | @mdf.js

                        Interface RetryOptions

                        Represents the options for retrying an operation

                        -
                        interface RetryOptions {
                            abortSignal?: AbortSignal;
                            attempts?: number;
                            interrupt?: (() => boolean);
                            logger?: LoggerFunction;
                            maxWaitTime?: number;
                            timeout?: number;
                            waitTime?: number;
                        }

                        Properties

                        abortSignal?: AbortSignal

                        The signal to be used to interrupt the retry process

                        -
                        attempts?: number

                        The maximum number of retry attempts.

                        -
                        interrupt?: (() => boolean)

                        A function that determines whether to interrupt the retry process -Should return true to interrupt, false otherwise.

                        -

                        User abortSignal instead

                        -

                        The logger function used for logging retry attempts

                        -
                        maxWaitTime?: number

                        The maximum time to wait between retry attempts, in milliseconds

                        -
                        timeout?: number

                        Timeout for each try

                        -
                        waitTime?: number

                        The time to wait between retry attempts, in milliseconds

                        -
                        diff --git a/docs/media/firehose-diagram.svg b/docs/media/firehose-diagram.svg new file mode 100644 index 00000000..a8f19faf --- /dev/null +++ b/docs/media/firehose-diagram.svg @@ -0,0 +1,3 @@ + + +

                        @mdf.js/firehose Module

                        Writable Stream

                        Writable Stream

                        Transform Stream
                        (Strategy)

                        Readable Stream

                        Readable Stream

                        Readable Stream

                        Data Flow

                        Data Flow

                        Data Flow

                        Pipe

                        Pipe

                        Pipe

                        Pipe

                        Pipe

                        Processed Data

                        Processed Data

                        Source.Plug.
                        Flow

                        Source.Plug.
                        Sequence

                        Source.Plug.
                        CreditFlow

                        Data Processing &
                        Transformation

                        Sink.Plug.
                        Tap

                        Sink.Plug.
                        Jet

                        Database or
                        Data Storage

                        Message Broker

                        Data Warehouse

                        Analytics Platform

                        \ No newline at end of file diff --git a/docs/media/logging-capture-1.png b/docs/media/logging-capture-1.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccdc9551f512bd21cf54582a915680dfd308f2c GIT binary patch literal 137218 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

                        uT5=Ea&#^+L1Qga4@_obqJ|qX*d1| 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

                        I%G;^pvIom8;B?g{mN=}ALt!ax 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 literal 0 HcmV?d00001 diff --git a/docs/modules.html b/docs/modules.html new file mode 100644 index 00000000..21a58d68 --- /dev/null +++ b/docs/modules.html @@ -0,0 +1 @@ +@mdf.js

                        diff --git a/docs/modules/_mdf.js_amqp-provider.Receiver.html b/docs/modules/_mdf.js_amqp-provider.Receiver.html new file mode 100644 index 00000000..980551fc --- /dev/null +++ b/docs/modules/_mdf.js_amqp-provider.Receiver.html @@ -0,0 +1,4 @@ +Receiver | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes

                        Port

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_amqp-provider.Sender.html b/docs/modules/_mdf.js_amqp-provider.Sender.html new file mode 100644 index 00000000..9244dec5 --- /dev/null +++ b/docs/modules/_mdf.js_amqp-provider.Sender.html @@ -0,0 +1 @@ +Sender | @mdf.js

                        Classes

                        Port

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_amqp-provider._internal_.html b/docs/modules/_mdf.js_amqp-provider._internal_.html new file mode 100644 index 00000000..2122dcd6 --- /dev/null +++ b/docs/modules/_mdf.js_amqp-provider._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js

                        Classes - Other

                        Container
                        Receiver
                        Sender

                        Classes - Provider

                        BasePort
                        diff --git a/docs/modules/_mdf.js_amqp-provider.html b/docs/modules/_mdf.js_amqp-provider.html new file mode 100644 index 00000000..d9c67ce5 --- /dev/null +++ b/docs/modules/_mdf.js_amqp-provider.html @@ -0,0 +1,162 @@ +@mdf.js/amqp-provider | @mdf.js

                        Module @mdf.js/amqp-provider

                        @mdf.js/amqp-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/amqp-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        AMQP provider for @mdf.js based on rhea.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/amqp-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/amqp-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        In this module there are implemented two providers:

                        +
                          +
                        • +

                          The consumer (Receiver), that wraps the rhea-promise Receiver, which wraps the rhea Receiver class.

                          +
                          import { Receiver } from '@mdf.js/amqp-provider';

                          const ownReceiver = Receiver.Factory.create({
                          name: `myAMQPReceiverName`,
                          config: {...}, //rhea - AMQP CommonConnectionOptions
                          logger: myLoggerInstance,
                          useEnvironment: true,
                          }); +
                          + +
                            +
                          • +

                            Defaults:

                            +
                            {
                            // ... Common client options, see below
                            receiver_options: {
                            name: process.env['NODE_APP_INSTANCE'] ||'mdf-amqp',
                            rcv_settle_mode: 0,
                            credit_window: 0,
                            autoaccept: false,
                            autosettle: true,
                            }
                            } +
                            + +
                          • +
                          • +

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            +
                            {
                            // ... Common client options, see below
                            receiver_options: {
                            name: process.env['CONFIG_AMQP_RECEIVER_NAME'],
                            rcv_settle_mode: process.env['CONFIG_AMQP_RECEIVER_SETTLE_MODE'], // coerced to number
                            credit_window: process.env['CONFIG_AMQP_RECEIVER_CREDIT_WINDOW'], // coerced to number
                            autoaccept: process.env['CONFIG_AMQP_RECEIVER_AUTO_ACCEPT'], // coerced to boolean
                            autosettle: process.env['CONFIG_AMQP_RECEIVER_AUTO_SETTLE'], // coerced to boolean
                            }
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          The producer (Sender) that wraps the rhea-promise AwaitableSender class.

                          +
                          import { Sender } from '@mdf.js/amqp-provider';

                          const ownSender = Sender.Factory.create({
                          name: `myAMQPSenderName`,
                          config: {...}, //rhea - AMQP CommonConnectionOptions
                          logger: myLoggerInstance,
                          useEnvironment: true,
                          }); +
                          + +
                            +
                          • +

                            Defaults:

                            +
                            {
                            // ... Common client options, see below
                            sender_options: {
                            name: process.env['NODE_APP_INSTANCE'] ||'mdf-amqp',
                            snd_settle_mode: 2,
                            autosettle: true,
                            target: {},
                            }
                            } +
                            + +
                          • +
                          • +

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            +
                            {
                            // ... Common client options, see below
                            sender_options: {
                            name: process.env['CONFIG_AMQP_SENDER_NAME'],
                            snd_settle_mode: process.env['CONFIG_AMQP_SENDER_SETTLE_MODE'], // coerced to number
                            autosettle: process.env['CONFIG_AMQP_SENDER_AUTO_SETTLE'], // coerced to boolean
                            }
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          Common client options:

                          +
                            +
                          • +

                            Defaults:

                            +
                            {
                            username: 'mdf-amqp',
                            host: '127.0.0.1',
                            port: 5672,
                            transport: 'tcp',
                            container_id: process.env['NODE_APP_INSTANCE'] || 'mdf-amqp',
                            reconnect: 5000,
                            initial_reconnect_delay: 30000,
                            max_reconnect_delay: 10000,
                            non_fatal_errors: ['amqp:connection:forced'],
                            idle_time_out: 5000,
                            reconnect_limit: Number.MAX_SAFE_INTEGER,
                            keepAlive: true,
                            keepAliveInitialDelay: 2000,
                            timeout: 10000,
                            all_errors_non_fatal: true,
                            } +
                            + +
                          • +
                          • +

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            +
                            {
                            username: process.env['CONFIG_AMQP_USER_NAME'],
                            password: process.env['CONFIG_AMQP_PASSWORD'],
                            host: process.env['CONFIG_AMQP_HOST'],
                            hostname: process.env['CONFIG_AMQP_HOSTNAME'],
                            port: process.env['CONFIG_AMQP_PORT'], // coerced to number
                            transport: process.env['CONFIG_AMQP_TRANSPORT'],
                            container_id: process.env['CONFIG_AMQP_CONTAINER_ID'],
                            id: process.env['CONFIG_AMQP_ID'],
                            reconnect: process.env['CONFIG_AMQP_RECONNECT'], // coerced to number
                            reconnect_limit: process.env['CONFIG_AMQP_RECONNECT_LIMIT'], // coerced to number
                            initial_reconnect_delay: process.env['CONFIG_AMQP_INITIAL_RECONNECT_DELAY'], // coerced to number
                            max_reconnect_delay: process.env['CONFIG_AMQP_MAX_RECONNECT_DELAY'], // coerced to number
                            max_frame_size: process.env['CONFIG_AMQP_MAX_FRAME_SIZE'], // coerced to number
                            non_fatal_errors: process.env['CONFIG_AMQP_NON_FATAL_ERRORS'], // coerced to array from string separated by ','
                            key: process.env['CONFIG_AMQP_CLIENT_KEY_PATH'], // The file will be read and the content will be used as the key
                            cert: process.env['CONFIG_AMQP_CLIENT_CERT_PATH'], // The file will be read and the content will be used as the cert
                            ca: process.env['CONFIG_AMQP_CA_PATH'], // The file will be read and the content will be used as the CA
                            requestCert: process.env['CONFIG_AMQP_REQUEST_CERT'], // coerced to boolean
                            rejectUnauthorized: process.env['CONFIG_AMQP_REJECT_UNAUTHORIZED'], // coerced to boolean
                            idle_time_out: process.env['CONFIG_AMQP_IDLE_TIME_OUT'], // coerced to number
                            keepAlive: process.env['CONFIG_AMQP_KEEP_ALIVE'], // coerced to boolean
                            keepAliveInitialDelay: process.env['CONFIG_AMQP_KEEP_ALIVE_INITIAL_DELAY'], // coerced to number
                            timeout: process.env['CONFIG_AMQP_TIMEOUT'], // coerced to number
                            all_errors_non_fatal: process.env['CONFIG_AMQP_ALL_ERRORS_NON_FATAL'], // coerced to boolean
                            }; +
                            + +
                          • +
                          +
                        • +
                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the status of the AMQP connection +
                            +
                          • observedValue: Actual state of the consumer/producer provider instance [error, running, stopped].
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          • output: in case of error state (status fail), the error message is shown.
                          • +
                          +
                        • +
                        • credits: Checks the credits of the AMQP connection +
                            +
                          • observedValue: Actual number of credits in the consumer/producer instance.
                          • +
                          • observedUnit: credits.
                          • +
                          • status: pass if the number of credits is greater than 0, warn otherwise.
                          • +
                          • output: No credits available if the number of credits is 0.
                          • +
                          +
                        • +
                        +
                        {
                        "[mdf-amqp:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "service",
                        "output": undefined
                        }
                        ],
                        "[mdf-amqp:credits]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": 10,
                        "observedUnit": "credits",
                        "output": undefined
                        }
                        ]
                        } +
                        + +
                          +
                        • CONFIG_AMQP_SENDER_NAME (default: NODE_APP_INSTANCE || `mdf-amqp`): The name of the link. This should be unique for the container. If not specified a unique name is generated.
                        • +
                        • CONFIG_AMQP_SENDER_SETTLE_MODE (default: 2): It specifies the sender settle mode with following possible values: - 0 - "unsettled" - The sender will send all deliveries initially unsettled to the receiver. - 1 - "settled" - The sender will send all deliveries settled to the receiver. - 2 - "mixed" - (default) The sender MAY send a mixture of settled and unsettled deliveries to the receiver.
                        • +
                        • CONFIG_AMQP_SENDER_AUTO_SETTLE (default: true): Whether sent messages should be automatically settled once the peer settles them.
                        • +
                        • CONFIG_AMQP_RECEIVER_NAME (default: NODE_APP_INSTANCE || `mdf-amqp`): The name of the link. This should be unique for the container. If not specified a unique name is generated.
                        • +
                        • CONFIG_AMQP_RECEIVER_SETTLE_MODE (default: 0): It specifies the receiver settle mode with following possible values: - 0 - "first" - The receiver will spontaneously settle all incoming transfers. - 1 - "second" - The receiver will only settle after sending the disposition to the sender and receiving a disposition indicating settlement of the delivery from the sender.
                        • +
                        • CONFIG_AMQP_RECEIVER_CREDIT_WINDOW (default: 0): A "prefetch" window controlling the flow of messages over this receiver. Defaults to 1000 if not specified. A value of 0 can be used to turn off automatic flow control and manage it directly.
                        • +
                        • CONFIG_AMQP_RECEIVER_AUTO_ACCEPT (default: false): Whether received messages should be automatically accepted.
                        • +
                        • CONFIG_AMQP_RECEIVER_AUTO_SETTLE (default: true): Whether received messages should be automatically settled once the remote settles them.
                        • +
                        • CONFIG_AMQP_USER_NAME (default: 'mdf-amqp'): User name for the AMQP connection
                        • +
                        • CONFIG_AMQP_PASSWORD (default: undefined): The secret key to be used while establishing the connection
                        • +
                        • CONFIG_AMQP_HOST (default: undefined): The hostname of the AMQP server
                        • +
                        • CONFIG_AMQP_HOSTNAME (default: 127.0.0.1): The hostname presented in open frame, defaults to host.
                        • +
                        • CONFIG_AMQP_PORT (default: 5672): The port of the AMQP server
                        • +
                        • CONFIG_AMQP_TRANSPORT (default: 'tcp'): The transport option. This is ignored if connection_details is set.
                        • +
                        • NODE_APP_INSTANCE (default: 'tcp'): The transport option. This is ignored if connection_details is set.
                        • +
                        • CONFIG_AMQP_CONTAINER_ID (default: process.env['NODE_APP_INSTANCE'] || `mdf-amqp`): The id of the source container. If not provided then this will be the id (a guid string) of the assocaited container object. When this property is provided, it will be used in the open frame to let the peer know about the container id. However, the associated container object would still be the same container object from which the connection is being created. The "container\_id" is how the peer will identify the 'container' the connection is being established from. The container in AMQP terminology is roughly analogous to a process. Using a different container id on connections from the same process would cause the peer to treat them as coming from distinct processes.
                        • +
                        • CONFIG_AMQP_ID (default: undefined): A unique name for the connection. If not provided then this will be a string in the following format: "connection-<counter>".
                        • +
                        • CONFIG_AMQP_RECONNECT (default: 5000): If true (default), the library will automatically attempt to reconnect if disconnected. If false, automatic reconnect will be disabled. If it is a numeric value, it is interpreted as the delay between reconnect attempts (in milliseconds).
                        • +
                        • CONFIG_AMQP_RECONNECT_LIMIT (default: undefined): Maximum number of reconnect attempts. Applicable only when reconnect is true.
                        • +
                        • CONFIG_AMQP_INITIAL_RECONNECT_DELAY (default: 30000): Time to wait in milliseconds before attempting to reconnect. Applicable only when reconnect is true or a number is provided for reconnect.
                        • +
                        • CONFIG_AMQP_MAX_RECONNECT_DELAY (default: 10000): Maximum reconnect delay in milliseconds before attempting to reconnect. Applicable only when reconnect is true.
                        • +
                        • CONFIG_AMQP_MAX_FRAME_SIZE (default: 4294967295): The largest frame size that the sending peer is able to accept on this connection.
                        • +
                        • CONFIG_AMQP_NON_FATAL_ERRORS (default: ['amqp:connection:forced']): An array of error conditions which if received on connection close from peer should not prevent reconnect (by default this only includes "amqp:connection:forced").
                        • +
                        • CONFIG_AMQP_NON_FATAL_ERRORS (default: ['amqp:connection:forced']): An array of error conditions which if received on connection close from peer should not prevent reconnect (by default this only includes "amqp:connection:forced").
                        • +
                        • CONFIG_AMQP_CA_PATH (default: undefined): The path to the CA certificate file
                        • +
                        • CONFIG_AMQP_CLIENT_CERT_PATH (default: undefined): The path to the client certificate file
                        • +
                        • CONFIG_AMQP_CLIENT_KEY_PATH (default: undefined): The path to the client key file
                        • +
                        • CONFIG_AMQP_REQUEST_CERT (default: false): If true the server will request a certificate from clients that connect and attempt to verify that certificate. Defaults to false.
                        • +
                        • CONFIG_AMQP_REJECT_UNAUTHORIZED (default: true): If true the server will reject any connection which is not authorized with the list of supplied CAs. This option only has an effect if requestCert is true.
                        • +
                        • CONFIG_AMQP_IDLE_TIME_OUT (default: 5000): The maximum period in milliseconds between activity (frames) on the connection that is desired from the peer. The open frame carries the idle-time-out field for this purpose. To avoid spurious timeouts, the value in idle_time_out is set to be half of the peer’s actual timeout threshold.
                        • +
                        • CONFIG_AMQP_KEEP_ALIVE (default: true): If true the server will send a keep-alive packet to maintain the connection alive.
                        • +
                        • CONFIG_AMQP_KEEP_ALIVE_INITIAL_DELAY (default: 2000): The initial delay in milliseconds for the keep-alive packet.
                        • +
                        • CONFIG_AMQP_TIMEOUT (default: 10000): The time in milliseconds to wait for the connection to be established.
                        • +
                        • CONFIG_AMQP_ALL_ERRORS_NON_FATAL (default: true): Determines if rhea's auto-reconnect should attempt reconnection on all fatal errors
                        • +
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Receiver
                        Sender
                        diff --git a/docs/modules/_mdf.js_core.Health.html b/docs/modules/_mdf.js_core.Health.html new file mode 100644 index 00000000..e725ea31 --- /dev/null +++ b/docs/modules/_mdf.js_core.Health.html @@ -0,0 +1,4 @@ +Health | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Enumerations

                        STATUS

                        Interfaces

                        Check

                        Type Aliases

                        CheckEntry
                        Checks
                        ComponentName
                        MeasurementName
                        Status

                        Variables

                        STATUSES

                        Functions

                        overallStatus
                        diff --git a/docs/modules/_mdf.js_core.Jobs.html b/docs/modules/_mdf.js_core.Jobs.html new file mode 100644 index 00000000..54ac51f4 --- /dev/null +++ b/docs/modules/_mdf.js_core.Jobs.html @@ -0,0 +1 @@ +Jobs | @mdf.js
                        diff --git a/docs/modules/_mdf.js_core.Layer.App.html b/docs/modules/_mdf.js_core.Layer.App.html new file mode 100644 index 00000000..8352c399 --- /dev/null +++ b/docs/modules/_mdf.js_core.Layer.App.html @@ -0,0 +1 @@ +App | @mdf.js
                        diff --git a/docs/modules/_mdf.js_core.Layer.Provider.html b/docs/modules/_mdf.js_core.Layer.Provider.html new file mode 100644 index 00000000..1ce30b14 --- /dev/null +++ b/docs/modules/_mdf.js_core.Layer.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/modules/_mdf.js_core.Layer.html b/docs/modules/_mdf.js_core.Layer.html new file mode 100644 index 00000000..009fb13c --- /dev/null +++ b/docs/modules/_mdf.js_core.Layer.html @@ -0,0 +1 @@ +Layer | @mdf.js

                        Namespaces

                        App
                        Provider

                        Type Aliases

                        Observable
                        diff --git a/docs/modules/_mdf.js_core._internal_.html b/docs/modules/_mdf.js_core._internal_.html new file mode 100644 index 00000000..177d5c7d --- /dev/null +++ b/docs/modules/_mdf.js_core._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js

                        State

                        State
                        diff --git a/docs/modules/_mdf.js_core.html b/docs/modules/_mdf.js_core.html new file mode 100644 index 00000000..aea1c545 --- /dev/null +++ b/docs/modules/_mdf.js_core.html @@ -0,0 +1,487 @@ +@mdf.js/core | @mdf.js

                        Module @mdf.js/core

                        @mdf.js/core

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/core

                        +
                        Core module with shared components for resource management and instrumentation API
                        + +
                        + +

                        The @mdf.js/core module is a set of types, interfaces, and classes that standardize the way in which the state of the resources managed by the applications developed with @mdf.js is reported. This module is part of the @mdf.js ecosystem, which aims to provide a set of tools and libraries that facilitate the development of applications in Node.js, especially those that require a high level of observability and monitoring.

                        +

                        The @mdf.js/core module is composed of the following elements:

                        +
                          +
                        • The Health interface, which is a set of types and interfaces that standardize the way in which the state of the resources is reported.
                        • +
                        • The Layer namespace, which contains the following elements: +
                            +
                          • The App interface, which is a set of types and interfaces that standardize the components of an application from the observability point of view.
                          • +
                          • The Provider API, which allows for the instrumentation of resource providers (databases, publish/subscribe services, etc.) so they can be managed in a standardized way within the @mdf.js API, especially in terms of observability, configuration management, and resource provider state management.
                          • +
                          +
                        • +
                        • The Jobs API, which allows for the creation and management of jobs in a standardized way within the @mdf.js API.
                        • +
                        +

                        To install the @mdf.js/core module, you can use the following commands:

                        +
                          +
                        • npm
                        • +
                        +
                        npm install @mdf.js/core
                        +
                        + +
                          +
                        • yarn
                        • +
                        +
                        yarn add @mdf.js/core
                        +
                        + +

                        The Health interface is a set of types and interfaces that standardize the way in which the state of the resources managed by the Provider is reported. The Health interface is composed of the following types and interfaces:

                        +
                          +
                        • +

                          Status: a type that represents the status of a resource, which can be one of the following values:

                          +
                            +
                          • pass: indicates that the resource is in a normal operating state.
                          • +
                          • fail: indicates that the resource is in an error state.
                          • +
                          • warn: indicates that the resource is in a warning state.
                          • +
                          +
                        • +
                        • +

                          Check<T>: an interface that defines the structure of a check object, with the following properties:

                          +
                            +
                          • componentId: a unique identifier for an instance of a specific sub-component/dependency of a service in UUID v4 format. Multiple objects with the same componentId may appear in the details if they are from different nodes.
                          • +
                          • componentType: an optional string that SHOULD be present if componentName is present. It indicates the type of the component, which could be a pre-defined value from the spec (such as component, datastore, or system), a common and standard term from a well-known source (like schema.org, IANA, or microformats), or a URI indicating extra semantics and processing rules.
                          • +
                          • observedValue: an optional property that could be any valid JSON value (such as a string, number, object, array, or literal). The type is referenced by T in the interface definition.
                          • +
                          • observedUnit: an optional string that SHOULD be present if metricValue is present. It could be a common and standard term from a well-known source or a URI indicating extra semantics and processing rules.
                          • +
                          • status: a value of type Status indicating whether the service status is acceptable or not.
                          • +
                          • affectedEndpoints: an optional array of strings containing URI Templates as defined by [RFC6570], indicating which particular endpoints are affected by the check.
                          • +
                          • time: an optional string indicating the date-time, in ISO8601 format, at which the reading of the metricValue was recorded.
                          • +
                          • output: an optional property containing raw error output in case of “fail” or “warn” states. This field SHOULD be omitted for “pass” state.
                          • +
                          • links: an optional object containing link relations and URIs [RFC3986] for external links that may contain more information about the health of the endpoint. This includes potentially a “self” link, which may be used by clients to check health via HTTP response code.
                          • +
                          • ... and any other property that the Provider developer considers necessary to provide more information about the check.
                          • +
                          +
                        • +
                        • +

                          Checks: The "checks" object within the health check model allows for the representation of the health status of various logical sub-components of a service. This flexible structure is designed to accommodate the complexities of modern distributed systems, where each component may consist of multiple nodes, each potentially exhibiting a different health status. Here's a breakdown of how the "checks" object is structured and the semantics of its keys and values:

                          +
                            +
                          • Each key in the "checks" object represents a logical sub-component of the service. The uniqueness of each key ensures that the health status of each sub-component can be individually assessed and reported.
                          • +
                          • The value associated with each key is an array of Check objects. This array accommodates scenarios where a single logical sub-component is supported by multiple nodes. For single-node sub-components, or when the distinction between nodes is not relevant, a single-element array is used for consistency.
                          • +
                          • The key for each sub-component is a unique string within the "details" section of the health check model. It may consist of two parts, separated by a colon (:): {componentName}:{metricName}. The structure of these keys is as follows: +
                              +
                            • componentName: This part of the key provides a human-readable identifier for the component. It must not contain a colon, as the colon serves as the delimiter between the component name and the metric name.
                            • +
                            • metricName: This part specifies the particular metric for which the health status is reported. Like the component name, it must not contain a colon. The metric name can be a pre-defined value specified by the health check model (such as "utilization," "responseTime," "connections," or "uptime"), a common term from a recognized standard or organization (like schema.org, IANA, or microformats), or a URI that conveys additional semantics and processing rules associated with the metric.
                            • +
                            +
                          • +
                          +

                          The Checks type is defined to capture this structure, where each entry in the object maps to an array of Check objects, allowing for a detailed and nuanced representation of the health status across different parts of a service and its underlying infrastructure.

                          +
                          export type Checks<T = any> = {
                          [entry in CheckEntry]: Check<T>[];
                          }; +
                          + +
                        • +
                        +

                        And finally, the Health export an auxiliary method overallStatus that determine the Status of the component based on the Checks object.

                        +
                        function overallStatus(checks: Checks): Status 
                        +
                        + +

                        The App interface is a set of types and interfaces that standardize the way in which the state of the application is reported. The App API define 3 different types of components from the observability point of view:

                        +
                          +
                        • +

                          Component: a component is any part of the system that has a own identity and can be monitored for error handling. The only requirement is to emit an error event when something goes wrong, to have a name and unique component identifier.

                          +
                          /** Component */
                          export interface Component extends EventEmitter {
                          /** Emitted when the component throw an error*/
                          on(event: 'error', listener: (error: Crash | Error) => void): this;
                          /** Component name */
                          name: string;
                          /** Component identifier */
                          componentId: string;
                          } +
                          + +

                          This interface define:

                          +
                            +
                          • Properties: +
                              +
                            • name: the name of the component, this name is used by the observability layers to identify the component.
                            • +
                            • componentId: a unique identifier for the instance in UUID v4 format.
                            • +
                            +
                          • +
                          • Events: +
                              +
                            • on('error', listener: (error: Crash | Error) => void): this: event emitted every time the Component emits an error.
                            • +
                            +
                          • +
                          +
                        • +
                        • +

                          Resource: a resource is extended component that represent the access to an external/internal resource, besides the error handling and identity, it has a start, stop and close methods to manage the resource lifecycle. It also has a checks property to define the checks that will be performed over the resource to achieve the resulted status. The most typical example of a resource are the Provider that allow to access to external databases, message brokers, etc.

                          +
                          /** Resource */
                          export interface Resource extends Component {
                          /** Emitted when the component throw an error*/
                          on(event: 'error', listener: (error: Crash | Error) => void): this;
                          /** Emitted on every status change */
                          on(event: 'status', listener: (status: Status) => void): this;
                          /** Checks performed over this component to achieve the resulted status */
                          checks: Checks;
                          /** Resource status */
                          status: Status;
                          /** Resource start function */
                          start: () => Promise<void>;
                          /** Resource stop function */
                          stop: () => Promise<void>;
                          /** Resource close function */
                          close: () => Promise<void>;
                          } +
                          + +

                          Besides the Component properties and events, this interface define:

                          +
                            +
                          • Properties: +
                              +
                            • checks: list of checks performed by the component to determine its state. It is a list of objects of type Health.Checks.
                            • +
                            • status: the current status of the Resource. It is a variable of type Health.Status whose value can be: +
                                +
                              • pass: indicates that the Resource is in a normal operating state. If all the checks are in pass state, the Resource will be in pass state.
                              • +
                              • fail: indicates that the Resource is in an error state. If any of the checks are in fail state, the Resource will be in fail state.
                              • +
                              • warn: indicates that the Resource is in a warning state. If any of the checks are in warn state, the Resource will be in warn state.
                              • +
                              +
                            • +
                            +
                          • +
                          • Methods: +
                              +
                            • start(): Promise<void>: initialize the Resource, internal jobs, external dependencies connections ....
                            • +
                            • stop(): Promise<void>: stops the Resource, close connections, stop internal jobs, etc.
                            • +
                            • close(): Promise<void>: closes the Resource, release resources, destroy connections, etc.
                            • +
                            +
                          • +
                          • Events: +
                              +
                            • on('status', listener: (status: Health.Status) => void): this: event emitted every time the Resource changes its state.
                            • +
                            +
                          • +
                          +
                        • +
                        • +

                          Service: a service is a special kind of resource that besides Resource properties, it could offer:

                          +
                            +
                          • Its own REST API endpoints, using an express router, to expose details about service.
                          • +
                          • A links property to define the endpoints that the service expose.
                          • +
                          • A metrics property to expose the metrics registry where the service will register its own metrics. This registry should be a prom-client registry.
                          • +
                          +
                          /** Service */
                          export interface Service extends Resource {
                          /** Express router */
                          router?: Router;
                          /** Service base path */
                          links?: Links;
                          /** Metrics registry */
                          metrics?: Registry;
                          } +
                          + +

                          Besides the Resource properties, methods and events, this interface define:

                          +
                            +
                          • Properties: +
                              +
                            • router: an express router that will be used to expose the service endpoints.
                            • +
                            • links: an object containing link relations and URIs [RFC3986] for external links that may contain more information about the health of the endpoint. This includes potentially a “self” link, which may be used by clients to check health via HTTP response code.
                            • +
                            • metrics: a metrics registry that will be used to register the service metrics. This registry should be a prom-client registry.
                            • +
                            +
                          • +
                          +
                        • +
                        +

                        The Provider API of @mdf.js allows for the instrumentation of resource providers (databases, publish/subscribe services, etc.) so they can be managed in a standardized way within the @mdf.js API, especially in terms of:

                        +
                          +
                        • Observability, as all Providers implement the Layer.App.Resource interface.
                        • +
                        • Configuration management, providing an interface for managing default, specific, or environment variable-based configurations.
                        • +
                        • Resource provider state management, through the standardization of the states and operation modes of the Providers.
                        • +
                        +

                        Some examples of providers instrumented with this API are:

                        + +

                        A provider that has been correctly instrumented with @mdf.js/core API always offers a Factory class with a single static method create that allows creating new instances of the provider with the desired configuration for each case.

                        +

                        This create method may receive a configuration object with the following optional properties:

                        +
                          +
                        • name: the name of the provider that will be used for observability, if not specified, the default provider name will be used.
                        • +
                        • logger: a LoggerInstance object, belonging to the @mdf.js/logger module or any other object that implements the LoggerInstance interface. If specified, it will be used by both the Provider and the Port it wraps. If not specified, a DEBUG type logger will be used with the provider's name indicated in the name property, or if not specified, with the default provider name.
                        • +
                        • config: specific configuration object for the module wrapped by the Provider in question. If not specified, the default configuration set by the Provider developer will be used.
                        • +
                        • useEnvironment: this property can be a boolean or a string, with its default value being false. It can take the following values: +
                            +
                          • boolean: +
                              +
                            • true: indicates that the environment variables defined by the Provider developer should be used, combined with the Provider's default values and the configuration passed as an argument. The configuration is established in this order of priority: first, the arguments provided directly are taken into account, then the configurations defined in the system's environment variables, and lastly, if none of the above is available, the default values are applied.
                            • +
                            • false: indicates that the environment variables defined by the Provider developer should NOT be used, only the default values will be combined with the configuration passed as an argument. In this case, the configuration is established in this order of priority: first, the arguments provided directly are taken into account, and then the default values.
                            • +
                            +
                          • +
                          • string: if a string is passed, it will be used as a prefix for the environment configuration variables, represented in SCREAMING_SNAKE_CASE, which will be transformed to camelCase and combined with the rest of the configuration, except with the environment variables defined by the Provider developer. In this case, the configuration is established in this order of priority: first, the arguments provided directly are taken into account, then the configurations defined in the system's environment variables, and lastly, if none of the above is available, the default values are applied.
                          • +
                          +
                        • +
                        +
                        +

                        Note: The aim of this configuration handling is to allow the user to work in two different modes:

                        +
                          +
                        • User rules: the user sets their own configuration, disregarding the environment variables indicated by the Provider developer, with the alternative of being able to use a fast track of environment variables usage through a prefix. That is: useEnvironment: false or useEnvironment: 'MY_PREFIX_'.
                        • +
                        • Provider rules: the user prefers to use the environment variables defined by the Provider developer, in which case the management of the environment variables should be delegated to the Provider, allowing the user to set specific configuration values through the input argument, an attempt to create a mixed configuration where both the Provider and the service/application try to use environment variables, can lead to undesirable situations. That is: useEnvironment: true.
                        • +
                        +
                        +
                        import { Mongo } from '@mdf.js/mongo-provider';
                        // Using only `Provider` default values:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create();
                        // Using `Provider` default values and custom values:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [x] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        });
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: false
                        });
                        // Using `Provider` default values, custom values and `Provider` environment variables:
                        // - [x] `Provider` default values
                        // - [x] `Provider` environment variables
                        // - [x] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: true
                        });
                        // Using `Provider` default values, custom values and `Provider` environment variables with a prefix:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [x] User custom values
                        // - [x] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: 'MY_PREFIX_'
                        }); +
                        + +

                        Now that we have our provider instance, let's see what it offers:

                        +
                          +
                        • Properties: +
                            +
                          • +

                            componentId: a unique identifier for the instance in UUID v4 format.

                            +
                          • +
                          • +

                            name: the name of the provider, by default, set by the Provider developer or the name provided in the configuration.

                            +
                          • +
                          • +

                            config: the resulting configuration that was used to create the instance.

                            +
                          • +
                          • +

                            state: the current state of the Provider. It is a variable of type ProviderState whose value can be:

                            +
                              +
                            • running: indicates that the Provider is in a normal operating state.
                            • +
                            • stopped: indicates that the Provider has been stopped or has not been initialized.
                            • +
                            • error: indicates that the Provider has encountered an error in its operation.
                            • +
                            +
                          • +
                          • +

                            error: in case the Provider is in an error state, this property will contain an object with the error information. This property is of type ProviderError, which is a type alias for Crash | Multi | undefined, you can find more information about Crash and Multi types in the documentation of @mdf.js/crash.

                            +
                          • +
                          • +

                            date: the date in ISO 8601 format of the last update of the Provider's state.

                            +
                          • +
                          • +

                            checks: list of checks performed by the Provider to determine its state. It is a list of objects of type Health.Checks, which will contain at least one entry with the information of the state check of the Provider under the property [${name}:status] where ${name} is the name of the Provider, indicated in the configuration or by default. This field will contain an array with a single object of type Health.Check that will contain the information of the state check of the Provider. Example value of checks:

                            +
                            {
                            "myName:status": [
                            {
                            "status": "pass", // "pass" | "fail" | "warn"
                            "componentId": "00000000-0000-0000-0000-000000000000", // UUID v4
                            "componentType": "connection", // or any other type indicated by the `Provider` developer
                            "observedValue": "running", // "running" | "stopped" | "error"
                            "time": "2024-10-10T10:10:10.000Z",
                            "output": undefined, // or the information of the error property
                            }
                            ]
                            } +
                            + +

                            The Provider developer can add more checks to the list of checks to provide more information about the state of the Provider or the resources it manages.

                            +
                          • +
                          • +

                            client: instance of the client/resource wrapped by the Provider that has been created with the provided configuration.

                            +
                          • +
                          +
                        • +
                        +
                        +

                        Note: the instance returned by the create method is an instance of Provider with generic types for the config (PortConfig) and client (PortClient) properties, which should be extended by the Provider developer to provide a better usage experience, so that the user can know both the configuration and the client that is being used, as well as the client that has been wrapped.

                        +
                        +
                          +
                        • Methods: +
                            +
                          • async start(): Promise<void>: starts the Provider and the resource it wraps.
                          • +
                          • async stop(): Promise<void>: stops the Provider and the resource it wraps.
                          • +
                          • async fail(error: Crash | Error): Promise<void>: sets the Provider in an error state and saves the error information.
                          • +
                          +
                        • +
                        • Events: +
                            +
                          • on('error', listener: (error: Crash | Error) => void): this: event emitted every time the Provider emits an error.
                          • +
                          • on('status', listener: (status: Health.Status) => void): this: event emitted every time the Provider changes its state.
                          • +
                          +
                        • +
                        +

                        To instrument a provider with the @mdf.js/core Provider API, the following actions must be taken:

                        +
                          +
                        • Use the abstract class Port, provided by the API, which must be extended by the Provider developer.
                        • +
                        • Define the properties of the PortConfigValidationStruct object, which indicate the default values of the Provider, the values coming from environment variables, and the validation object of type 'Schema' from the Joi module.
                        • +
                        • Use the ProviderFactoryCreator function to create an instance of the Provider, the class of type Mixin that standardizes the creation of Provider instances with the desired configuration.
                        • +
                        +

                        Let's see point by point how the instrumentation of a Provider is carried out.

                        +

                        The Port should be extended to implement a new specific Port. This class implements some util logic to facilitate the creation of new Ports, for this reason is exposed as abstract class, instead of an interface. The developer should keep the constructor signature, in order to maintain the compatibility with the ProviderFactoryCreator function.

                        +

                        class diagram

                        +

                        The basic operations that already implemented in the class are:

                        +
                          +
                        • Properties: +
                            +
                          • uuid: create by the Port class, it is a unique identifier for the port instance, this uuid is used in error traceability.
                          • +
                          • name: the name of the port, by default, set by the Provider developer or the name provided in the configuration.
                          • +
                          • config: the resulting configuration that was used to create the Port instance.
                          • +
                          • logger: a LoggerInstance object, belonging to the @mdf.js/logger module or any other object that implements the LoggerInstance interface. This property is used to log information about the port and the resources it manages. The Port class set the context of the logger to the port name and the uuid, so it's not necessary to include the context and the uuid of the port in the log messages.
                          • +
                          • checks: list of checks performed by the Port by the use of addCheck method, these checks are collected by the Provider, together with the own check of status, and offered to the observability layers.
                          • +
                          +
                        • +
                        +
                        +

                        Note: As the signature of the Port constructor should maintained:

                        +
                        constructor(config: PortConfig, logger: LoggerInstance, name: string)
                        +
                        + +

                        Your Port class extension will receive the config, logger and name properties, and you should call the super constructor with these properties.

                        +
                        +

                        What the developers of the Provider should develop in their own Port class extension is:

                        +
                          +
                        • async start(): Promise<void> method, which is responsible initialize or stablish the connection to the resources.
                        • +
                        • async stop(): Promise<void> method, which is responsible stop services or disconnect from the resources.
                        • +
                        • async close(): Promise<void> method, which is responsible to destroy the services, resources or perform a simple disconnection.
                        • +
                        • state property, a boolean value that indicates if the port is connected (true) or healthy (true) or not (false).
                        • +
                        • client property, that return the PortClient instance that is used to interact with the resources.
                        • +
                        +

                        In the next example you can see the expected behavior of a Port class extension when the start, stop and close methods are called depending on the state of the port:¡.

                        +

                        class diagram

                        +

                        In the other hand, this class extends the EventEmitter class, so it's possible to emit events to notify the status of the port:

                        +
                          +
                        • on('error', listener: (error: Crash) => void): this: should be emitted to notify errors in the resource management or access, this will not change the provider state, but the error will be registered in the observability layers.
                        • +
                        • on('closed', listener: (error?: Crash) => void): this: should be emitted if the access to the resources is not longer possible. This event should not be emitted when stop or close methods are used. If the event includes an error, the provider will indicate this error as the cause of the port closure and will be registered in the observability layers.
                        • +
                        • on('unhealthy', listener: (error: Crash) => void): this: should be emitted when the port has limited access or no access to the resources, but the provider is still running and trying to recover the access. If the event includes an error, the provider will indicate this error as the cause of the port unhealthiness and will be registered in the observability layers.
                        • +
                        • on('healthy', listener: () => void): this: should be emitted when the port has recovered the access to the resources.
                        • +
                        +

                        class diagram

                        +

                        Check some examples of implementation in:

                        + +

                        The PortConfigValidationStruct object is a type that defines the default values of the Provider, the values coming from environment variables, and the validation object of type 'Schema' from the Joi module.

                        +

                        The PortConfigValidationStruct object should have the following properties:

                        +
                          +
                        • defaultConfig: an object with the default values of the Provider.
                        • +
                        • envBaseConfig: an object with the environment variables that the Provider will use, if any.
                        • +
                        • schema: a Joi schema object that will be used to validate the configuration object passed to the Provider.
                        • +
                        +
                        import Joi from 'joi';

                        export const PortConfigValidationStruct = {
                        defaultConfig: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        envBaseConfig: {
                        url: process.env['MONGO_URL'],
                        appName: process.env['MONGO_APP_NAME'],
                        },
                        schema: Joi.object({
                        url: Joi.string().uri().required(),
                        appName: Joi.string().required(),
                        }),
                        }; +
                        + +

                        The ProviderFactoryCreator function is a utility function that allows creating instances of the Provider with the desired configuration. This function receives the following arguments:

                        +
                          +
                        • port: the class that extends the Port class and implements the specific Provider.
                        • +
                        • validation: the PortConfigValidationStruct object that defines the default values, environment variables, and the validation schema of the Provider.
                        • +
                        • defaultName: the default name of the Provider, so that it will be used if the name is not provided in the configuration.
                        • +
                        • type: the type of the Provider, which will be used to identify the kind of Provider in the observability layers.
                        • +
                        +
                        const Factory = ProviderFactoryCreator(MongoPort, myConfig, 'Mongo', 'database');
                        +
                        + +

                        The Factory object returned by the ProviderFactoryCreator function has a single static method create that allows creating new instances of the Provider with the desired configuration for each case.

                        +
                        import { Layer } from '@mdf.js/core';
                        import { LoggerInstance } from '@mdf.js/logger';
                        import { CONFIG_PROVIDER_BASE_NAME } from '../config';
                        import { Client, Config } from './types';

                        export type Client = Console;
                        export type Config = {}

                        export class Port extends Layer.Provider.Port<Client, Config> {
                        /** Client handler */
                        private readonly instance: Client;
                        /** */
                        private interval: NodeJS.Timeout;
                        /**
                        * Implementation of functionalities of an HTTP client port instance.
                        * @param config - Port configuration options
                        * @param logger - Port logger, to be used internally
                        * @param name - Port name, to be used in the logger
                        */
                        constructor(config: Config, logger: LoggerInstance, name: string) {
                        super(config, logger, name);
                        this.instance = console;
                        this.interval = setInterval(this.myCheckFunction, 1000);
                        }
                        /** Stupid check function */
                        private readonly myCheckFunction = (): void => {
                        // Check the client status
                        this.addCheck('myCheck', {
                        status: 'pass',
                        componentId: this.uuid,
                        componentType: 'console',
                        observedValue: 'im stupid',
                        time: new Date().toISOString(),
                        });
                        // Emit the status event
                        this.emit('healthy');
                        }
                        /** Return the underlying port instance */
                        public get client(): Client {
                        return this.instance;
                        }
                        /** Return the port state as a boolean value, true if the port is available, false in otherwise */
                        public get state(): boolean {
                        return true;
                        }
                        /** Initialize the port instance */
                        public async start(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        /** Stop the port instance */
                        public async stop(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        /** Close the port instance */
                        public async close(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        } +
                        + +
                        import { Layer } from '@mdf.js/core';
                        import { Config } from './port';
                        import Joi from 'joi';

                        export const config: Layer.Provider.PortConfigValidationStruct<Config> = {
                        defaultConfig: {},
                        envBaseConfig: {},
                        schema: Joi.object({}),
                        }; +
                        + +
                        import { Layer } from '@mdf.js/core';
                        import { configEntry } from '../config';
                        import { Port, Client, Config } from './port';

                        export const Factory = Layer.Provider.ProviderFactoryCreator<Client, Config, Port>(
                        Port,
                        configEntry,
                        `myConsole`,
                        'console'
                        ); +
                        + +
                        import { Factory } from './factory';

                        const myProvider = Factory.create();
                        myProvider.instance.log('Hello world!');
                        console.log(myProvider.state); // true
                        console.log(myProvider.checks); // { "myConsole:status": [{ status: 'pass', ... }], { "myConsole:myCheck": [{ status: 'pass', ... }] }
                        myProvider.on('healthy', () => {
                        console.log('Im healthy');
                        }); +
                        + +

                        The Jobs API from @mdf.js allows for the management of job requests and executions within an @mdf.js application in a simplified and standardized way. The two main elements of this API are:

                        +
                          +
                        • The JobHandler class, which is responsible for "transporting" the information of the jobs to be executed, as well as notifying the execution thereof through events to interested observers.
                        • +
                        • The JobRequest interface defines the structure of job requests.
                        • +
                        +
                        class JobHandler<
                        Type extends string = string,
                        Data = unknown,
                        CustomHeaders extends Record<string, any> = NoMoreHeaders,
                        CustomOptions extends Record<string, any> = NoMoreOptions,
                        >;
                        interface JobRequest<
                        Type extends string = string,
                        Data = unknown,
                        CustomHeaders extends Record<string, any> = NoMoreHeaders,
                        CustomOptions extends Record<string, any> = NoMoreOptions,
                        >; +
                        + +

                        Both the class, JobHandler, and the interface, JobRequest use generic types to define the structure of the data transported in the jobs, as well as to define the custom headers and options that can be added to the jobs. In this way, the Jobs API is flexible and can be used in different contexts and with different types of data. The generic parameters for the JobHandler class and the JobRequest interface are as follows:

                        +
                          +
                        • Type: a string type representing the type or types of job to be executed. This string type can be used to filter the jobs to be executed, so that only jobs of a specific type are executed, or to apply different execution logic depending on the job type. For example, it can be used to execute notification jobs: email, sms, push, etc, so the generic type Type would be declared as type Type = 'email' | 'sms' | 'push'.
                        • +
                        • Data: a generic type representing the structure of the data transported in the jobs. This type can be any type of data, from a primitive type like a number or a string, to a complex object with multiple properties. For example, if you want to send an email, the data could be an object with the properties to, subject, and body.
                        • +
                        • CustomHeaders: a generic type representing the custom headers that can be added to the jobs. This type must be a key-value map, where the key is a string and the value can be any type of data. These custom headers can be used to add additional information to the jobs, such as metadata, authentication information, etc. Custom headers are optional and it is not necessary to add them to the jobs if not needed. By default, the generic type CustomHeaders is NoMoreHeaders, which is a type that does not allow adding custom headers to the jobs. An example of a custom header could be an authentication header containing an access token to an external API: { Authorization: 'Bearer <access token>' }.
                        • +
                        • CustomOptions: a generic type representing the custom options that can be added to the jobs. This type must be a key-value map, where the key is a string and the value can be any type of data. These custom options can be used to add additional information to the jobs, such as specific configurations, execution parameters, etc. Custom options are optional and it is not necessary to add them to the jobs if not needed. By default, the generic type CustomOptions is NoMoreOptions, which is a type that does not allow adding custom options to the jobs. In addition to the custom options, there is the property numberOfHandlers, read the section on the JobHandler class for more information.
                        • +
                        +

                        An example of customizing the generic types of the JobHandler class and the JobRequest interface would be as follows:

                        +
                        import { Jobs } from '@mdf.js/core';
                        type Type = 'email' | 'sms' | 'push';
                        type Data = { to: string; subject: string; body: string };
                        type CustomHeaders = { Authorization: string };
                        type CustomOptions = { retry: number };

                        export type MyOwnJobRequest = Jobs.JobRequest<Type, Data, CustomHeaders, CustomOptions>;
                        export class MyOwnJobHandler extends Jobs.JobHandler<Type, Data, CustomHeaders, CustomOptions> {}

                        const myHandler = new MyOwnJobHandler('multi', { to: '', body: '', subject: '' }, 'email', {
                        headers: { Authorization: '' },
                        retry: 0,
                        });

                        const myHandler2 = new MyOwnJobHandler({
                        data: { to: '', body: '', subject: '' },
                        type: 'email',
                        jobUserId: '123',
                        options: { headers: { Authorization: '' }, retry: 0 },
                        }); +
                        + +

                        Let's look in more detail at the structure of the JobHandler class:

                        +
                          +
                        • +

                          constructor: there are two ways to instantiate a JobHandler:

                          +
                            +
                          • constructor(jobRequest: JobRequest<Type, Data, CustomHeaders>): by using a JobRequest object that contains the information of the job to be executed.
                          • +
                          • constructor(jobUserId: string, data: Data, type?: Type, options?: Options<CustomHeaders>): by using the necessary parameters to create a JobRequest object. +Ultimately, both cases are equivalent, as the JobRequest object contains the same data as the constructor parameters. Thus, we can analyze the parameters for creation through the JobRequest object: +
                              +
                            • type: a string type representing the type of job to be executed. The type of this variable is of the generic type Type, read the section on customization of generic types for more information.
                            • +
                            • data: a generic type representing the structure of the data transported in the jobs. The type of this variable is of the generic type Data, read the section on customization of generic types for more information.
                            • +
                            • options: this parameter is optional and is used to add custom headers or options to the jobs. The type of this variable is an object containing two properties: +
                                +
                              • headers: an object containing the custom headers that will be added to the job. By default, this property is of the type CustomHeaders, read the section on customization of generic types for more information.
                              • +
                              • numberOfHandlers: an integer number indicating the number of handlers that will be used to execute the job. By default, this property is 1, which means that the job has to be confirmed, through the use of the done method, only once. If a value greater than 1 is set, the job has to be confirmed n times, where n is the value of numberOfHandlers.
                              • +
                              +
                            • +
                            • jobUserId: an identifier for the job. It should be used to identify the job in the user's logic. When identifying a job, keep in mind the following: +
                                +
                              • Property uuid: each new instance of JobHandler has a unique identifier that can be accessed only in read mode through the property uuid.
                              • +
                              • Property type: indicates the type of job to be executed.
                              • +
                              • Property jobUserId: identifier of the job that should be used to identify the job in the user's logic. +That is, we can have several jobs whose jobUserId is alarmNotification, being able to have each one of them a different type or not and being all instances uniquely identifiable by their uuid.
                              • +
                              +
                            • +
                            +
                          • +
                          +
                        • +
                        • +

                          Properties:

                          +
                            +
                          • uuid: a unique identifier for the instance of the JobHandler. This identifier is read-only and is generated automatically when creating a new instance of the JobHandler.
                          • +
                          • type: a string type representing the type of job to be executed. This type is of the generic type Type, read the section on customization of generic types for more information.
                          • +
                          • jobUserId: an identifier for the job. It should be used to identify the job in the user's logic.
                          • +
                          • jobUserUUID: is a UUID v5 hash that is generated from the jobUserId. This hash is read-only and is generated automatically when creating a new instance of the JobHandler.
                          • +
                          • status: an enumerated type of Status that indicates the status of the job. The possible values are: +
                              +
                            • Status.PENDING(pending): indicates that the job is pending execution. It is the initial state of a job.
                            • +
                            • Status.PROCESSING(processing): indicates that the job is being processed.
                            • +
                            • Status.COMPLETED(completed): indicates that the job has been completed successfully.
                            • +
                            • Status.FAILED(failed): indicates that the job has failed.
                            • +
                            +
                          • +
                          • data: a generic type representing the structure of the data transported in the jobs. This type is of the generic type Data. When accessing the property for the first time, i.e., when the status is Status.PENDING, the job changes its status to Status.PROCESSING.
                          • +
                          • options: contains the options indicated in the constructor of the class.
                          • +
                          • createdAt: the creation date of the job as a Date object.
                          • +
                          • hasErrors: a boolean value that indicates if the job contains errors. This value is read-only and is set automatically when an error occurs in the job. These errors are included through the done and addError methods.
                          • +
                          • errors: if there are errors in the job, this property contains a Multi object belonging to the @mdf.js/crash module that contains the information of the errors. This property is read-only and is set automatically when an error occurs in the job. These errors are included through the done and addError methods.
                          • +
                          • processTime: if the job has been completed successfully, this property contains the time it took to process the job in milliseconds, otherwise, the value is -1.
                          • +
                          +
                        • +
                        • +

                          Methods:

                          +
                            +
                          • +

                            public addError(error: Crash | Multi): void: adds an error to the job. This method is used to add errors to the job that have occurred during its execution. The errors added through this method are included in the errors property and the hasErrors property is set to true. The error created is of the type ValidationError.

                            +
                          • +
                          • +

                            public done(error?: Crash): void: finishes the job. This method is used to finish the job and change its status to Status.COMPLETED if no error has occurred, or to Status.FAILED if an error has occurred. If an error is provided, it is added to the errors property and the hasErrors property is set to true. This method will have to be called as many times as numberOfHandlers has been set in the constructor, once the number of calls is reached, the job will emit the done event.

                            +
                          • +
                          • +

                            public result(): Result<Type>: returns a Result object containing the information of the job.

                            +
                            /** Job result interface */
                            export interface Result<Type extends string = string> {
                            /** Unique job processing identification */
                            uuid: string;
                            /** Job type */
                            type: Type;
                            /** Timestamp, in ISO format, of the job creation date */
                            createdAt: string;
                            /** Timestamp, in ISO format, of the job resolve date */
                            resolvedAt: string;
                            /** Number of entities processed with success in this job */
                            quantity: number;
                            /** Flag that indicate that the publication process has some errors */
                            hasErrors: boolean;
                            /** Array of errors */
                            errors?: MultiObject;
                            /** User job request identifier, defined by the user */
                            jobUserId: string;
                            /** Unique user job request identification, based on jobUserId */
                            jobUserUUID: string;
                            /** Job status */
                            status: Status;
                            } +
                            + +
                          • +
                          • +

                            public toObject(): JobObject<Type, Data, CustomHeaders, CustomOptions>: returns a JobObject type object containing the information of the job.

                            +
                            /** Job object interface */
                            export interface JobObject<
                            Type extends string = string,
                            Data = any,
                            CustomHeaders extends Record<string, any> = NoMoreHeaders,
                            CustomOptions extends Record<string, any> = NoMoreOptions,
                            > extends JobRequest<Type, Data, CustomHeaders, CustomOptions> {
                            /** Job type identification, used to identify specific job handlers to be applied */
                            type: Type;
                            /** Unique job processing identification */
                            uuid: string;
                            /** Unique user job request identification, generated by UUID V5 standard and based on jobUserId */
                            jobUserUUID: string;
                            ** Job status */
                            status: Status;
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          Events:

                          +
                            +
                          • on(event: 'done', listener: (uuid: string, result: Result<Type>, error?: Multi) => void): this: event emitted when the job has been completed successfully or has failed. The event returns the unique identifier of the job, the information of the job in a Result object type, and an error in case a failure has occurred.
                          • +
                          +
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Health
                        Jobs
                        Layer
                        diff --git a/docs/modules/_mdf.js_crash._internal_.html b/docs/modules/_mdf.js_crash._internal_.html new file mode 100644 index 00000000..644283a7 --- /dev/null +++ b/docs/modules/_mdf.js_crash._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_crash.html b/docs/modules/_mdf.js_crash.html new file mode 100644 index 00000000..29f188a4 --- /dev/null +++ b/docs/modules/_mdf.js_crash.html @@ -0,0 +1,116 @@ +@mdf.js/crash | @mdf.js

                        Module @mdf.js/crash

                        @mdf.js/crash

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        @mdf.js/crash

                        +
                        Improved, but simplified, error handling
                        + +
                        + +

                        The goal of @mdf.js/crash is to provide improved, but simplified, error handling, while standardizing error handling across all MMS modules.

                        +
                          +
                        • npm
                        • +
                        +
                        npm install @mdf.js/crash
                        +
                        + +
                          +
                        • yarn
                        • +
                        +
                        yarn add @mdf.js/crash
                        +
                        + +

                        This library provides us with 3 different types of errors to use depending on the context in which we find ourselves:

                        +
                          +
                        • Crash: it is the main type of error, it does not allow adding metadata to the error, which will be specially treated by the logging libraries, as well as relating errors to their causes.
                        • +
                        • Multi: it is the type of error mainly used in validation processes in which we can have more than one error that prevents an information input or a group of parameters from being validated. This type of error is the one returned to us by the @ netin-js / doorkeeper validation libraries.
                        • +
                        • Boom: this type of error standardizes errors in RESTful environments by providing helpers that allow the easy creation of standardized responses to frontend applications.
                        • +
                        +

                        One of the main and common parameters of the three types of error is the message associated with the error. This message indicates the type of error that occurred, always taking into account the good practices:

                        +
                          +
                        • Be clear and unambiguous.
                        • +
                        • Be concise and provide accurate information and only what is necessary.
                        • +
                        • Don't use technical jargon.
                        • +
                        • Be humble, don't blame the user.
                        • +
                        • Avoid using negative words.
                        • +
                        • Indicates the way to fix it to the user.
                        • +
                        • Do not use capital letters.
                        • +
                        • Indicates the correct actions, if any.
                        • +
                        • If there are details about the error, provide them in the corresponding section.
                        • +
                        +

                        According to the standard RFC 4122, it allows us to associate an identifier of operation or request to the error. This is especially useful for tracing errors in requests or transactions that occur between different systems or libraries. The identifier should be created by the process or system that initiates the operation (Frontend, Service ...), being included in all processes and registers (logging with @mdf.js/logger) so that this can be used in troubleshooting processes. identifier as filter, allowing easy extraction.

                        +

                        Logging example

                        +

                        Simple example of using the Crash error type.

                        +
                        import { Crash } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const enhancedError = new Crash('Example', v4());
                        console.log(enhancedError.message); // 'Example' +
                        + +

                        or even simpler

                        +
                        import { Crash } from '@mdf.js/crash'

                        const enhancedError = new Crash('Example');
                        console.log(enhancedError.message); // 'Example' +
                        + +

                        Crash allows us to add extra information about the error that can be used by higher layers of our application or at the time of recording the errors.

                        +
                        import { Crash } from './Crash';
                        import fs from 'fs';
                        import { v4 } from 'uuid';

                        const operationId = v4();
                        try {
                        const myContent = fs.readFileSync('path/to/file');
                        } catch (error) {
                        const enhancedError = new Crash(`Error reading the configuration file`, operationId, {
                        cause: error as Error,
                        name: 'FileError',
                        info: {
                        path: 'path/to/file',
                        },
                        });
                        console.log(enhancedError.trace());
                        // [ 'FileError: Error reading the configuration file',
                        // 'caused by Error: ENOENT: no such file or directory, open \'path/to/file\'' ]
                        } +
                        + +

                        Crash allows us to easily determine if an error has a specific cause, being able to act differently for each cause.

                        +
                        ourPromiseThatRejectCrash()
                        .then(()=>{
                        // Our code in case of success
                        })
                        .catch(error => {
                        if (error.hasCauseWithName('FileError')) {
                        // Our code in case of file error
                        } else {
                        // Our code for the rest type of errors
                        }
                        }) +
                        + +

                        Simple example of using Multi type error.

                        +
                        import { Multi } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const enhancedError = new Multi('Example', v4(), {
                        causes: [new Error('My first check that fail'), new Error('My Second check that fail')]
                        }); +
                        + +

                        Errors can be added later, which can be especially useful in transformation processes where various errors can appear during execution.

                        +
                        import { Multi, Crash } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const arrayOfNumbers: number[] = [];
                        const operationId = v4();
                        let enhancedError: Multi | undefined;
                        for (let idx = 0; idx < 10; idx++) {
                        arrayOfNumbers.push(Math.random() * (10 - 0) + 0);
                        }
                        for (const entry of arrayOfNumbers) {
                        if (entry > 5) {
                        const newError = new Crash(`Number of of range`, operationId, {
                        name: 'ValidationError',
                        info: {
                        number: entry,
                        },
                        });
                        if (enhancedError) {
                        enhancedError.push(newError);
                        } else {
                        enhancedError = new Multi(`Errors during validation process`, operationId, {
                        causes: [newError],
                        });
                        }
                        }
                        }
                        if (enhancedError) {
                        console.log(enhancedError.trace());
                        } +
                        + +

                        The most typical way to use the Boom type of error is through helpers, thanks to them, we can create information-rich errors, within the context of our REST API, in a simple way.

                        +
                        import express from 'express';
                        import { BoomHelpers } from '@mdf.js/crash';
                        import { v4 } from 'uuid';
                        const app = express();
                        const port = 3000;

                        app.get('/', (req, res) => {
                        const enhancedError = BoomHelpers.internalServerError('Error during request processing', v4());
                        res.status(enhancedError.status).json(enhancedError);
                        });

                        app.listen(port, () => {
                        console.log(`Example app listening at http://localhost:${port}`)
                        }); +
                        + +

                        Any request to the previous endpoint will return the following result:

                        +
                        {
                        "uuid": "2a931651-6921-4bda-864e-123b69829cff",
                        "status": 500,
                        "code": "HTTP",
                        "title": "Internal Server Error",
                        "detail": "Error during request processing"
                        } +
                        + +

                        We can even provide more information to the user through the options.

                        +
                        import express from 'express';
                        import { BoomHelpers, Crash } from '@mdf.js/crash';
                        import { v4 } from 'uuid';
                        const app = express();
                        const port = 3000;

                        const mock = (req: express.Request, res: express.Response, next: express.NextFunction): void => {
                        req.body = {};
                        req.body.reqId = v4();
                        req.body.order = 'myOrder';
                        next();
                        };

                        function getOrder(order: string, uuid: string): Promise<void> {
                        return Promise.reject(
                        new Crash(`The requested record is not present in the system`, uuid, {
                        name: 'DataNotPresent',
                        info: { order },
                        })
                        );
                        }

                        app.use(mock);
                        app.get('/order', (req, res) => {
                        getOrder(req.body.order, req.body.reqId)
                        .then(result => {
                        res.status(200).json(result);
                        })
                        .catch(error => {
                        const enhancedError = BoomHelpers.badRequest(
                        'Error getting the requested order',
                        req.body.reqId,
                        {
                        cause: error,
                        source: {
                        pointer: req.path,
                        parameters: { order: req.body.order },
                        },
                        name: error.name,
                        info: {
                        detail: error.message,
                        },
                        links: {
                        help: 'help/link/about/orders',
                        },
                        }
                        );
                        res.status(enhancedError.status).json(enhancedError);
                        });
                        });

                        app.listen(port, () => {
                        console.log(`Example app listening at http://localhost:${port}`);
                        }); +
                        + +

                        Any request to the previous endpoint will return the following result:

                        +
                        {
                        "uuid": "59fe72ec-44dc-4cc3-84ec-46c98df00283",
                        "links": {
                        "help": "help/link/about/orders"
                        },
                        "status": 400,
                        "code": "DataNotPresent",
                        "title": "Bad Request",
                        "detail": "Error getting the requested order",
                        "source": {
                        "pointer": "/order",
                        "parameters": {
                        "order": "myOrder"
                        }
                        },
                        "meta": {
                        "detail": "The requested record is not present in the system"
                        }
                        } +
                        + + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Crash

                        Crash
                        CrashObject
                        CrashOptions

                        Multi

                        Multi
                        MultiObject
                        MultiOptions

                        Boom

                        Boom
                        BoomHelpers
                        APIError
                        APISource
                        BoomOptions
                        ContextLink
                        Links
                        SimpleLink

                        Joi integration

                        Context
                        ValidationError
                        ValidationErrorItem

                        Other

                        <internal>
                        Cause
                        diff --git a/docs/modules/_mdf.js_doorkeeper.html b/docs/modules/_mdf.js_doorkeeper.html new file mode 100644 index 00000000..1e2987e5 --- /dev/null +++ b/docs/modules/_mdf.js_doorkeeper.html @@ -0,0 +1,51 @@ +@mdf.js/doorkeeper | @mdf.js

                        Module @mdf.js/doorkeeper

                        @mdf.js/doorkeeper

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        @mdf.js/doorkeeper

                        +
                        Improved, but simplified, JSON Schema validation using AJV
                        + +
                        + +

                        The goal of @mdf.js/doorkeeper is to provide a simple and robust solution for validating, registering, and managing JSON schemas in diverse applications. The code is designed to leverage advanced JSON schema validation using AJV (Another JSON Schema Validator), enriched with additional functionalities.

                        +
                          +
                        • npm
                        • +
                        +
                        npm install @mdf.js/doorkeeper
                        +
                        + +
                          +
                        • yarn
                        • +
                        +
                        yarn add @mdf.js/doorkeeper
                        +
                        + +

                        This package is part of the @mdf.js project, a collection of packages for building applications with Node.js and Typescript.

                        +

                        @mdf.js/doorkeeper has been designed to store and manage all the JSON schemas used in an application, allowing to assign a unique identifier to each schema, which it is associated with concrete type or interface, in this way, it is possible to validate the data of the application in a simple and robust way and to obtain the type of the data for Typescript applications.

                        +
                        import { Doorkeeper } from '@mdf.js/doorkeeper';

                        export interface User {
                        name: string;
                        age: number;
                        };

                        export interface Address {
                        street: string;
                        city: string;
                        country: string;
                        };

                        const userSchema = {
                        type: 'object',
                        properties: {
                        name: { type: 'string' },
                        age: { type: 'number' },
                        },
                        required: ['name', 'age'],
                        additionalProperties: false,
                        };

                        const addressSchema = {
                        type: 'object',
                        properties: {
                        street: { type: 'string' },
                        city: { type: 'string' },
                        country: { type: 'string' },
                        },
                        required: ['street', 'city', 'country'],
                        additionalProperties: false,
                        };

                        export interface Schemas {
                        'User': User;
                        'Address': Address;
                        }

                        const checker = new Doorkeeper<Schemas>().register({
                        'User': userSchema,
                        'Address': addressSchema,
                        });

                        const user: User = {
                        name: 'John',
                        age: 30,
                        };

                        const address: Address = {
                        street: 'Main Street',
                        city: 'New York',
                        country: 'USA',
                        };

                        const myNewUser = await checker.validate('User', user); // myNewUser is of type User
                        const myNewAddress = await checker.validate('Address', address); // myNewAddress is of type Address +
                        + + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Doorkeeper

                        DoorKeeper

                        Other

                        DoorkeeperOptions
                        ResultCallback
                        SchemaSelector
                        ValidatedOutput
                        diff --git a/docs/modules/_mdf.js_elastic-provider.Elastic.html b/docs/modules/_mdf.js_elastic-provider.Elastic.html new file mode 100644 index 00000000..6433d5cb --- /dev/null +++ b/docs/modules/_mdf.js_elastic-provider.Elastic.html @@ -0,0 +1,4 @@ +Elastic | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_elastic-provider._internal_.html b/docs/modules/_mdf.js_elastic-provider._internal_.html new file mode 100644 index 00000000..0c4b7225 --- /dev/null +++ b/docs/modules/_mdf.js_elastic-provider._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_elastic-provider.html b/docs/modules/_mdf.js_elastic-provider.html new file mode 100644 index 00000000..1d5f5b30 --- /dev/null +++ b/docs/modules/_mdf.js_elastic-provider.html @@ -0,0 +1,100 @@ +@mdf.js/elastic-provider | @mdf.js

                        Module @mdf.js/elastic-provider

                        @mdf.js/elastic-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/elastic-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        Elasticsearch provider for @mdf.js based on elasticsearch-js.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/elastic-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/elastic-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        The provider implemented in this module wraps the elasticsearch-js client.

                        +
                        import { Elastic } from '@mdf.js/elastic-provider';

                        const elastic = Elastic.Factory.create({
                        name: `myElasticProvider`,
                        config: {...}, // elasticsearch-js - `ClientOptions`
                        logger: myLoggerInstance,
                        useEnvironment: true,
                        }); +
                        + +
                          +
                        • +

                          Defaults:

                          +
                          {
                          nodes: ['http://localhost:9200'],
                          maxRetries: 5,
                          requestTimeout: 30000,
                          pingTimeout: 3000,
                          resurrectStrategy: 'ping',
                          name: process.env['NODE_APP_INSTANCE'] || 'mdf-elastic',
                          } +
                          + +
                        • +
                        • +

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          +
                          {
                          nodes: process.env['CONFIG_ELASTIC_NODES'] || process.env['CONFIG_ELASTIC_NODE'], // If CONFIG_ELASTIC_NODES is set, CONFIG_ELASTIC_NODE is ignored. CONFIG_ELASTIC_NODES is split by ','
                          maxRetries: process.env['CONFIG_ELASTIC_MAX_RETRIES'],
                          requestTimeout: process.env['CONFIG_ELASTIC_REQUEST_TIMEOUT'],
                          pingTimeout: process.env['CONFIG_ELASTIC_PING_TIMEOUT'],
                          resurrectStrategy: process.env['CONFIG_ELASTIC_RESURRECT_STRATEGY'],
                          tls: {
                          ca: CA, // file loaded from process.env['CONFIG_ELASTIC_CA_PATH']
                          cert: CERT, // file loaded from process.env['CONFIG_ELASTIC_CLIENT_CERT_PATH']
                          key: KEY, // file loaded from process.env['CONFIG_ELASTIC_CLIENT_KEY_PATH']
                          rejectUnauthorized: process.env['CONFIG_ELASTIC_HTTP_SSL_VERIFY'],
                          servername: process.env['CONFIG_ELASTIC_TLS_SERVER_NAME'],
                          },
                          name: process.env['CONFIG_ELASTIC_NAME'],
                          auth, // { username: process.env['CONFIG_ELASTIC_AUTH_USERNAME'], password: process.env['CONFIG_ELASTIC_AUTH_PASSWORD'] } if both are set
                          proxy: process.env['CONFIG_ELASTIC_PROXY'],
                          } +
                          + +
                        • +
                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the status of the Elasticsearch nodes using the cat health API and evaluating the number of nodes in red state. +
                            +
                          • observedValue: Actual state of the consumer/producer provider instance [error, running, stopped].
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          • output: in case of error state (status fail), the error message is shown.
                          • +
                          +
                        • +
                        • nodes: Checks and shows the number of nodes in the cluster using the cat nodes API. +
                            +
                          • observedValue: response from the cat nodes API in JSON format.
                          • +
                          • observedUnit: Nodes Health.
                          • +
                          • status: pass if no nodes are in red state, fail in other case.
                          • +
                          • output: in case of fail state At least one of the nodes in the system is red state.
                          • +
                          +
                        • +
                        +
                        {
                        "[mdf-elastic:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "database",
                        "output": undefined,
                        },
                        ],
                        "[mdf-elastic:nodes]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": { ... },
                        "observedUnit": "Nodes Health",
                        "output": undefined,
                        },
                        ],
                        } +
                        + +
                          +
                        • CONFIG_ELASTIC_NODE (default: undefined): Node to connect to. If CONFIG_ELASTIC_NODES is set, this is ignored.
                        • +
                        • CONFIG_ELASTIC_NODES (default: ['http://localhost:9200']): List of nodes to connect to. If this is set, CONFIG_ELASTIC_NODE is ignored.
                        • +
                        • CONFIG_ELASTIC_MAX_RETRIES (default: 5): Maximum number of retries before failing the request.
                        • +
                        • CONFIG_ELASTIC_REQUEST_TIMEOUT (default: 30000): Time in milliseconds before the request is considered a timeout.
                        • +
                        • CONFIG_ELASTIC_PING_TIMEOUT (default: 3000): Time in milliseconds before the request is considered a timeout.
                        • +
                        • CONFIG_ELASTIC_PROXY (default: undefined): Proxy to use when connecting to the Elasticsearch cluster.
                        • +
                        • CONFIG_ELASTIC_NAME (default: CONFIG_ARTIFACT_ID): Name of the Elasticsearch client.
                        • +
                        • CONFIG_ELASTIC_HTTP_SSL_VERIFY (default: true): Whether to verify the SSL certificate.
                        • +
                        • CONFIG_ELASTIC_CA_PATH (default: undefined): Path to the CA certificate.
                        • +
                        • CONFIG_ELASTIC_CLIENT_CERT_PATH (default: undefined): Path to the client certificate.
                        • +
                        • CONFIG_ELASTIC_CLIENT_KEY_PATH (default: undefined): Path to the client key.
                        • +
                        • CONFIG_ELASTIC_TLS_SERVER_NAME (default: undefined): Server name for the TLS certificate.
                        • +
                        • CONFIG_ELASTIC_AUTH_USERNAME (default: undefined): Username for the Elasticsearch cluster. If this is set, a password must also be provided.
                        • +
                        • CONFIG_ELASTIC_AUTH_PASSWORD (default: undefined): Password for the Elasticsearch cluster. If this is set, a username must also be provided.
                        • +
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Elastic
                        diff --git a/docs/modules/_mdf.js_faker._internal_.html b/docs/modules/_mdf.js_faker._internal_.html new file mode 100644 index 00000000..bd45725b --- /dev/null +++ b/docs/modules/_mdf.js_faker._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js

                        Interfaces

                        Entry

                        Type Aliases

                        GeneratorOptions
                        diff --git a/docs/modules/_mdf.js_faker.html b/docs/modules/_mdf.js_faker.html new file mode 100644 index 00000000..f5d5217f --- /dev/null +++ b/docs/modules/_mdf.js_faker.html @@ -0,0 +1,102 @@ +@mdf.js/faker | @mdf.js

                        Module @mdf.js/faker

                        @mdf.js/faker

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js

                        +
                        Typescript tools for development
                        + +
                        + +

                        Faker is a tool to generate fake data for testing purposes based on rosie package with some improvements.

                        +

                        To use @mdf.js/faker you first define a factory. The factory is defined in terms of attributes, sequences, options, callbacks, and can inherit from other factories. Once the factory is defined you use it to build objects.

                        +
                          +
                        • npm
                        • +
                        +
                        npm install @mdf.js/faker
                        +
                        + +
                          +
                        • yarn
                        • +
                        +
                        yarn add @mdf.js/faker
                        +
                        + +

                        There are two phases of use:

                        +
                          +
                        1. Factory definition
                        2. +
                        3. Object building
                        4. +
                        +

                        Factory Definition: Define a factory, specifying attributes, sequences, options, and callbacks:

                        +
                        import { Factory } from '@mdf.js/faker';
                        interface Player {
                        id: number;
                        name: string;
                        position: string;
                        }

                        interface Game {
                        id: number;
                        is_over: boolean;
                        created_at: Date;
                        random_seed: number;
                        players: Player[];
                        }

                        const playerFactory = new Factory<Player>()
                        .sequence('id')
                        .sequence('name', i => {
                        return 'player' + i;
                        })

                        // Define `position` to depend on `id`.
                        .attr('position', ['id'], id => {
                        const positions = ['pitcher', '1st base', '2nd base', '3rd base'];
                        return positions[id % positions.length];
                        });

                        const gameFactory = new Factory<Game>()
                        .sequence('id')
                        .attr('is_over', false)
                        .attr('created_at', () => new Date())
                        .attr('random_seed', () => Math.random())
                        // Default to two players. If players were given, fill in
                        // whatever attributes might be missing.
                        .attr('players', ['players'], players => {
                        if (!players) {
                        players = [{}, {}];
                        }
                        return players.map(data => playerFactory.build(data));
                        });

                        const disabledPlayer = new Factory().extend(playerFactory).attr('state', 'disabled'); +
                        + +

                        Object Building: Build an object, passing in attributes that you want to override:

                        +
                        const game = gameFactory.build({ is_over: true });
                        // Built object (note scores are random):
                        //{
                        // id: 1,
                        // is_over: true, // overriden when building
                        // created_at: Fri Apr 15 2011 12:02:25 GMT-0400 (EDT),
                        // random_seed: 0.8999513240996748,
                        // players: [
                        // {id: 1, name:'Player 1'},
                        // {id: 2, name:'Player 2'}
                        // ]
                        //} +
                        + +

                        You can specify options that are used to programmatically generate the attributes:

                        +
                        import { Factory } from '@mdf.js/faker';
                        import moment from 'moment';

                        interface Match {
                        matchDate: string;
                        homeScore: number;
                        awayScore: number;
                        }

                        const matchFactory = new Factory<Match>()
                        .attr('seasonStart', '2016-01-01')
                        .option('numMatches', 2)
                        .attr('matches', ['numMatches', 'seasonStart'], (numMatches, seasonStart) => {
                        const matches = [];
                        for (const i = 1; i <= numMatches; i++) {
                        matches.push({
                        matchDate: moment(seasonStart).add(i, 'week').format('YYYY-MM-DD'),
                        homeScore: Math.floor(Math.random() * 5),
                        awayScore: Math.floor(Math.random() * 5),
                        });
                        }
                        return matches;
                        });

                        matchFactory.build({ seasonStart: '2016-03-12' }, { numMatches: 3 });
                        // Built object (note scores are random):
                        //{
                        // seasonStart: '2016-03-12',
                        // matches: [
                        // { matchDate: '2016-03-19', homeScore: 3, awayScore: 1 },
                        // { matchDate: '2016-03-26', homeScore: 0, awayScore: 4 },
                        // { matchDate: '2016-04-02', homeScore: 1, awayScore: 0 }
                        // ]
                        //} +
                        + +

                        In the example numMatches is defined as an option, not as an attribute. Therefore numMatches is not part of the output, it is only used to generate the matches array.

                        +

                        In the same example seasonStart is defined as an attribute, therefore it appears in the output, and can also be used in the generator function that creates the matches array.

                        +

                        The convenience function attrs simplifies the common case of specifying multiple attributes in a batch. Rewriting the game example from above:

                        +
                        const gameFactory = new Factory()
                        .sequence('id')
                        .attrs({
                        is_over: false,
                        created_at: () => new Date(),
                        random_seed: () => Math.random(),
                        })
                        .attr('players', ['players'], players => {
                        /* etc. */
                        }); +
                        + +

                        You can also define a callback function to be run after building an object:

                        +
                        interface Coach {
                        id: number;
                        players: Player[];
                        }

                        const coachFactory = new Factory()
                        .option('buildPlayer', false)
                        .sequence('id')
                        .attr('players', ['id', 'buildPlayer'], (id, buildPlayer) => {
                        if (buildPlayer) {
                        return [Factory.build('player', { coach_id: id })];
                        }
                        })
                        .after((coach, options) => {
                        if (options.buildPlayer) {
                        console.log('built player:', coach.players[0]);
                        }
                        });

                        Factory.build({}, { buildPlayer: true }); +
                        + +

                        Multiple callbacks can be registered, and they will be executed in the order they are registered. The callbacks can manipulate the built object before it is returned to the callee.

                        +

                        If the callback doesn't return anything, @mdf.js/faker will return build object as final result. If the callback returns a value, @mdf.js/faker will use that as final result instead.

                        +

                        This is an advanced use case that you can probably happily ignore, but store this away in case you need it.

                        +

                        When you define a factory you can optionally provide a class definition, and anything built by the factory will be passed through the constructor of the provided class.

                        +

                        Specifically, the output of .build is used as the input to the constructor function, so the returned object is an instance of the specified class:

                        +
                        class SimpleClass {
                        constructor(args) {
                        this.moops = 'correct';
                        this.args = args;
                        }

                        isMoopsCorrect() {
                        return this.moops;
                        }
                        }

                        testFactory = Factory.define('test', SimpleClass).attr('some_var', 4);

                        testInstance = testFactory.build({ stuff: 2 });
                        console.log(JSON.stringify(testInstance, {}, 2));
                        // Output:
                        // {
                        // "moops": "correct",
                        // "args": {
                        // "stuff": 2,
                        // "some_var": 4
                        // }
                        // }

                        console.log(testInstance.isMoopsCorrect());
                        // Output:
                        // correct +
                        + +

                        Mind. Blown.

                        +

                        To use @mdf.js/faker in node, you'll need to import it first:

                        +
                        import { Factory } from '@mdf.js/faker';
                        // or with `require`
                        const Factory = require('@mdf.js/faker').Factory; +
                        + +

                        You might also choose to use unregistered factories, as it fits better with node's module pattern:

                        +
                        // factories/game.js
                        import { Factory } from '@mdf.js/faker';

                        export default new Factory().sequence('id').attr('is_over', false);
                        // etc +
                        + +

                        To use the unregistered Game factory defined above:

                        +
                        import Game from './factories/game';

                        const game = Game.build({ is_over: true }); +
                        + +

                        You can also extend an existing unregistered factory:

                        +
                        // factories/scored-game.js
                        import { Factory } from '@mdf.js/faker';
                        import Game from './game';

                        export default new Factory().extend(Game).attrs({
                        score: 10,
                        }); +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Classes

                        Factory

                        Interfaces

                        DefaultObject
                        DefaultOptions

                        Type Aliases

                        Builder
                        DefaultValue
                        Dependencies
                        diff --git a/docs/modules/_mdf.js_file-flinger._internal_.html b/docs/modules/_mdf.js_file-flinger._internal_.html new file mode 100644 index 00000000..17b2f0b3 --- /dev/null +++ b/docs/modules/_mdf.js_file-flinger._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_file-flinger.html b/docs/modules/_mdf.js_file-flinger.html new file mode 100644 index 00000000..9b3918f8 --- /dev/null +++ b/docs/modules/_mdf.js_file-flinger.html @@ -0,0 +1,300 @@ +@mdf.js/file-flinger | @mdf.js

                        Module @mdf.js/file-flinger

                        @mdf.js/file-flinger

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/file-flinger

                        +
                        Module designed to facilitate data file processing for cold path ingestion
                        + +
                        + +

                        @mdf.js/file-flinger is a robust module within the @mdf.js ecosystem, designed to facilitate customized data file processing workflows for cold path ingestion. It provides a versatile framework for constructing file processing pipelines, enabling developers to define custom pushers to deliver data files to various destinations.

                        +

                        Before delving into the documentation, it is essential to understand the core concepts within @mdf.js/file-flinger:

                        +
                          +
                        • Keygen: A key generation utility that creates unique identifiers for processed files based on customizable patterns. This feature is crucial for tracking and managing data files across the platform by generating consistent and meaningful identifiers.
                        • +
                        • Pusher: A pusher is a component that sends processed files to a specific destination. Developers can create custom pushers to integrate with various data storage solutions, such as databases, cloud storage, or data lakes. The file-flinger module supports multiple pushers, allowing users to define different destinations for processed files concurrently.
                        • +
                        • Watcher: The watcher module monitors directories for incoming files and triggers processing workflows when new files are detected. It plays a pivotal role in automating data ingestion tasks by initiating processing as soon as new data arrives.
                        • +
                        • Post-processing tasks: After processing a file, it is possible to execute a set of tasks to clean up the file system, move files to another location, archive them, or perform other operations. This post-processing can be different for completed and failed files, providing flexibility in handling different outcomes.
                        • +
                        +
                          +
                        • +

                          npm:

                          +
                          npm install @mdf.js/file-flinger
                          +
                          + +
                        • +
                        • +

                          yarn:

                          +
                          yarn add @mdf.js/file-flinger
                          +
                          + +
                        • +
                        +

                        To build a file processing workflow, you need to create custom pushers that send processed files to specific destinations. A pusher is responsible for delivering files to storage systems like databases, cloud storage, or data lakes.

                        +

                        To create a pusher, you need to define a class that implements the Pusher interface. This interface extends Layer.App.Resource, which provides methods and properties for managing the resource lifecycle and health status.

                        +

                        When implementing a pusher, you should ensure the following:

                        +
                          +
                        • Lifecycle Methods: Implement start(), stop(), and close() methods to manage the pusher's lifecycle.
                        • +
                        • Push Method: Implement the push(filePath: string, key: string) method, which handles the logic to send the file to the destination using the provided file path and key.
                        • +
                        • Metrics: Provide a metrics getter that returns a Prometheus Registry containing the pusher's metrics.
                        • +
                        • Health Status: Provide status and checks getters that return the pusher's health status and checks, which are crucial for monitoring the pusher's health.
                        • +
                        +

                        If you are using the @mdf.js framework to create your pusher, you can integrate the pusher's health information with the provider's health information.

                        +

                        Here is an example of a custom pusher class:

                        +
                        import { EventEmitter } from 'events';
                        import { Pusher } from '@mdf.js/file-flinger';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        // Class that implements the Pusher interface
                        class MyCustomPusher extends EventEmitter implements Pusher {
                        /** Constructor */
                        constructor() {
                        super();
                        }

                        /**
                        * Push the file to the storage
                        * @param filePath - The file path to push
                        * @param key - The key to use
                        */
                        public async push(filePath: string, key: string): Promise<void> {
                        // Implementation of file pushing logic
                        }

                        /** Start the pusher and the underlying provider */
                        public async start(): Promise<void> {
                        // Initialization logic
                        }

                        /** Stop the pusher and the underlying provider */
                        public async stop(): Promise<void> {
                        // Graceful shutdown logic
                        }

                        /** Stop the pusher and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Cleanup logic
                        }

                        /** Prometheus registry to store the metrics of the pusher */
                        public get metrics(): Registry {
                        // Return Prometheus registry with pusher metrics
                        return new Registry();
                        }

                        /** Pusher health status */
                        public get status(): Health.Status {
                        // Return health status
                        return 'pass';
                        }

                        /** Pusher health checks */
                        public get checks(): Health.Checks {
                        // Return object with health checks
                        return {};
                        }
                        } +
                        + +

                        The FileFlinger class extends EventEmitter and emits several events that you can listen to:

                        +
                          +
                        • +

                          error: Emitted when the component detects an error.

                          +
                          fileFlinger.on('error', (error) => {
                          console.error('An error occurred:', error);
                          }); +
                          + +
                        • +
                        • +

                          status: Emitted when the component's status changes.

                          +
                          fileFlinger.on('status', (status) => {
                          console.log('FileFlinger status:', status);
                          }); +
                          + +
                        • +
                        +

                        To instantiate a FileFlinger, you need to provide a name and an options object that configures its behavior. The options include:

                        +
                          +
                        • pushers: An array of pushers that will be used to send files to their destinations.
                        • +
                        • watchPath: The path or array of paths to monitor for incoming files.
                        • +
                        • filePattern (default: undefined): A glob pattern or custom pattern to match the files to be processed.
                        • +
                        • keyPattern (default: {_filename}): A pattern used by the key generator (Keygen) to create unique keys for the files.
                        • +
                        • defaultValues (default: {}): An object containing default values for placeholders used in patterns.
                        • +
                        • cwd (default: undefined): The base directory for relative paths.
                        • +
                        • maxErrors (default: 10): The maximum number of errors to store in the error list.
                        • +
                        • retryDelay (default: 30000): Delay in milliseconds between retries for failed file processing operations.
                        • +
                        • archiveFolder (default: undefined): The directory where processed files will be moved if the post-processing strategy is archive or zip.
                        • +
                        • deadLetterFolder (default: undefined): The directory where files with processing errors will be moved if the error strategy is dead-letter.
                        • +
                        • postProcessingStrategy (default: 'delete'): Strategy for handling files after successful processing. Options are: +
                            +
                          • 'delete': Delete the file.
                          • +
                          • 'archive': Move the file to the archiveFolder.
                          • +
                          • 'zip': Compress the file and move it to the archiveFolder.
                          • +
                          +
                        • +
                        • errorStrategy (default: 'delete'): Strategy for handling files that encountered errors during processing. Options are: +
                            +
                          • 'delete': Delete the file.
                          • +
                          • 'ignore': Leave the file as is.
                          • +
                          • 'dead-letter': Move the file to the deadLetterFolder.
                          • +
                          +
                        • +
                        • retryOptions: Configuration for retrying file operations. Includes: +
                            +
                          • attempts (default: 3): Number of retry attempts.
                          • +
                          • maxWaitTime (default: 60000): Maximum total wait time in milliseconds between retries.
                          • +
                          • timeout (default: 10000): Timeout in milliseconds for each retry attempt.
                          • +
                          • waitTime (default: 1000): Initial wait time in milliseconds between retries, which may be increased based on a backoff strategy.
                          • +
                          +
                        • +
                        +

                        Here's how to create a FileFlinger instance with custom options:

                        +
                        import { FileFlinger } from '@mdf.js/file-flinger';

                        const fileFlinger = new FileFlinger('MyFileFlinger', {
                        pushers: [/* Your custom pushers */],
                        watchPath: '/path/to/watch',
                        filePattern: '{sensor}_{measurement}_{date}.jsonl',
                        keyPattern: '{sensor}/{measurement}/{date}',
                        defaultValues: {},
                        cwd: process.cwd(),
                        maxErrors: 10,
                        retryDelay: 30000,
                        archiveFolder: '/path/to/archive',
                        deadLetterFolder: '/path/to/dead-letter',
                        postProcessingStrategy: 'archive',
                        errorStrategy: 'dead-letter',
                        retryOptions: {
                        attempts: 3,
                        maxWaitTime: 60000,
                        timeout: 10000,
                        waitTime: 1000,
                        },
                        }); +
                        + +

                        To manage the lifecycle of the FileFlinger, you can use the following methods:

                        +
                          +
                        • start(): Promise<void>: Starts the FileFlinger, initializing all watchers and pushers, and begins processing files as they arrive.
                        • +
                        • stop(): Promise<void>: Stops the FileFlinger, gracefully shutting down all watchers and pushers.
                        • +
                        • close(): Promise<void>: Stops the FileFlinger and cleans up all resources, including closing any open file handles or network connections.
                        • +
                        +

                        Example:

                        +
                        // Start the FileFlinger
                        await fileFlinger.start();

                        // The FileFlinger is now monitoring for files and processing them.

                        // When you need to stop the FileFlinger
                        await fileFlinger.stop();

                        // If you want to completely close and clean up resources
                        await fileFlinger.close(); +
                        + +

                        The FileFlinger class includes a Prometheus Registry to store metrics related to the file processing pipeline. These metrics can be used to monitor the performance and health of the system.

                        +

                        Default metrics included in the FileFlinger are:

                        +
                          +
                        • api_all_job_processed_total: The total number of jobs processed, labeled by type.
                        • +
                        • api_all_errors_job_processing_total: The total number of errors encountered during job processing, labeled by type.
                        • +
                        • api_all_job_in_processing_total: The number of jobs currently being processed, labeled by type.
                        • +
                        • api_publishing_job_duration_milliseconds: The duration of file processing jobs in milliseconds, labeled by type.
                        • +
                        +

                        The type label typically represents the key generated for the file, allowing you to categorize metrics by file type or other meaningful identifiers.

                        +

                        Pushers should also provide metrics and health information. They should implement the metrics, status, and checks properties:

                        +
                          +
                        • metrics: Returns a Prometheus Registry with the pusher's metrics.
                        • +
                        • status: Returns the health status of the pusher ('pass' or 'fail').
                        • +
                        • checks: Returns an object containing detailed health checks for the pusher.
                        • +
                        +

                        You can access the FileFlinger's metrics and health information:

                        +
                        // Access metrics
                        const metricsRegistry = fileFlinger.metrics;

                        // Access health status and checks
                        const fileFlingerStatus = fileFlinger.status;
                        const fileFlingerChecks = fileFlinger.checks; +
                        + +

                        The Keygen utility is responsible for generating unique and meaningful identifiers (keys) for processed files. These keys are used to identify and track files within the system and are crucial for organizing data in storage destinations.

                        +

                        The key generation process involves:

                        +
                          +
                        1. Parsing the File Name: Extract placeholders from the file name using a specified filePattern.
                        2. +
                        3. Generating Predefined Placeholders: Create a set of predefined placeholders based on the current date and time.
                        4. +
                        5. Merging Placeholders: Combine default values, parsed placeholders, and predefined placeholders into a single set.
                        6. +
                        7. Generating the Key: Replace placeholders in the keyPattern with actual values from the merged placeholders to produce the final key.
                        8. +
                        +

                        Placeholders are enclosed in curly braces {} and are used in both the filePattern and keyPattern. They are replaced with actual values during key generation.

                        +

                        The following placeholders are available by default:

                        +
                          +
                        • {_filename}: The base name of the file without its extension.
                        • +
                        • {_extension}: The file extension (including the dot), e.g., .jsonl.
                        • +
                        • {_timestamp}: The current timestamp in milliseconds since the Unix epoch.
                        • +
                        • {_date}: The current date in YYYY-MM-DD format.
                        • +
                        • {_time}: The current time in HH:mm:ss format.
                        • +
                        • {_datetime}: The current date and time in YYYY-MM-DD_HH-mm-ss format.
                        • +
                        • {_year}: The current year as a four-digit number.
                        • +
                        • {_month}: The current month as a two-digit number (01-12).
                        • +
                        • {_day}: The current day of the month as a two-digit number (01-31).
                        • +
                        • {_hour}: The current hour as a two-digit number (00-23).
                        • +
                        • {_minute}: The current minute as a two-digit number (00-59).
                        • +
                        • {_second}: The current second as a two-digit number (00-59).
                        • +
                        +

                        You can define custom placeholders by specifying them in the filePattern. These placeholders extract corresponding values from the file name.

                        +

                        Example:

                        +
                          +
                        • File Name: sensor1_temperature_2023-10-24.jsonl
                        • +
                        • File Pattern: {sensor}_{measurement}_{date}.jsonl
                        • +
                        • Extracted Placeholders: sensor, measurement, date
                        • +
                        +

                        The Keygen class accepts an options object to customize its behavior:

                        +
                          +
                        • filePattern: A pattern used to parse the file name and extract placeholders.
                        • +
                        • keyPattern: A pattern used to generate the key by replacing placeholders with actual values.
                        • +
                        • defaultValues: An object containing default values for placeholders that may not be present in the file name.
                        • +
                        +

                        Default Options:

                        +
                        const DEFAULT_KEY_GEN_OPTIONS: Required<KeygenOptions> = {
                        filePattern: '*', // Matches any file name
                        keyPattern: '{_filename}', // Uses the file name without extension as the key
                        defaultValues: {}, // No default values provided
                        }; +
                        + +

                        Description: Generate a key using default settings.

                        +
                        # filePattern: undefined
                        keyPattern: '{_filename}'
                        # defaultValues: {} +
                        + +
                          +
                        • Filename: myfile.txt
                        • +
                        • Key: myfile
                        • +
                        +

                        Explanation:

                        +
                          +
                        • Since filePattern is undefined, any file name matches.
                        • +
                        • The keyPattern {_filename} uses the file name without the extension.
                        • +
                        • The key generated is 'myfile'.
                        • +
                        +

                        Description: Generate a key by extracting custom placeholders from the file name.

                        +
                        filePattern: '{sensor}_{measurement}_{date}.jsonl'
                        keyPattern: '{sensor}/{measurement}/{date}'
                        # defaultValues: {} +
                        + +
                          +
                        • Filename: sensor1_temperature_2023-10-24.jsonl
                        • +
                        • Key: sensor1/temperature/2023-10-24
                        • +
                        +

                        Explanation:

                        +
                          +
                        • The filePattern extracts sensor, measurement, and date from the file name.
                        • +
                        • The keyPattern constructs the key using these placeholders.
                        • +
                        +

                        Description: Use default values for placeholders not present in the file name.

                        +
                        filePattern: '{sensor}_{measurement}_{date}.jsonl'
                        keyPattern: '{sensor}/{measurement}/{date}/{location}'
                        defaultValues:
                        location: 'defaultLocation' +
                        + +
                          +
                        • Filename: sensor1_temperature_2023-10-24.jsonl
                        • +
                        • Key: sensor1/temperature/2023-10-24/defaultLocation
                        • +
                        +

                        Explanation:

                        +
                          +
                        • The location placeholder is not present in the file name.
                        • +
                        • The defaultValues provide a value for location.
                        • +
                        +

                        Description: Generate a key that includes current date components.

                        +
                        filePattern: '{sensor}_{measurement}.jsonl'
                        keyPattern: '{sensor}/{measurement}/{_year}/{_month}/{_day}'
                        # defaultValues: {} +
                        + +
                          +
                        • Filename: sensor1_temperature.jsonl
                        • +
                        • Key: sensor1/temperature/2024/11/03
                        • +
                        +

                        Explanation:

                        +
                          +
                        • The placeholders {_year}, {_month}, {_day} are replaced with the current date components.
                        • +
                        +

                        Description: Generate a key using complex file patterns and default values.

                        +
                        filePattern: '{sensor}_{measurement}_{year}-{month}-{day}_{end}.jsonl'
                        keyPattern: '{sensor}/{measurement}/{year}/{month}/{day}/data_{source}'
                        defaultValues:
                        source: 'myFileFlinger1' +
                        + +
                          +
                        • Filename: mySensor_flowMeter1_2024-12-30_2024-12-31.jsonl
                        • +
                        • Key: mySensor/flowMeter1/2024/12/30/data_myFileFlinger1
                        • +
                        +

                        Explanation:

                        +
                          +
                        • Custom placeholders sensor, measurement, year, month, day, and end are extracted from the file name.
                        • +
                        • The source placeholder is provided via defaultValues.
                        • +
                        +

                        During key generation, some errors can occur. These errors are emitted as error events and can be handled by listening to the FileFlinger's error event.

                        +
                          +
                        • +

                          Filename Does Not Match Pattern: If the file name does not match the filePattern, an error is emitted.

                          +

                          Error Message: 'Filename [invalid_filename.jsonl] does not match the pattern [{sensor}_{measurement}_{date}.jsonl]'

                          +
                        • +
                        • +

                          Placeholder Not Found in Values: If a placeholder in the keyPattern is not found in the merged placeholders, an error is emitted.

                          +

                          Error Message: 'Error generating a key based on pattern [{sensor}/{measurement}/{date}/{unknown}] for file [sensor1_temperature_2023-10-24.jsonl]: Placeholder [unknown] not found in values'

                          +
                        • +
                        +
                          +
                        • Define Both filePattern and keyPattern: Explicitly specify these patterns to ensure keys are generated as expected.
                        • +
                        • Ensure Consistency: Make sure placeholders used in keyPattern are either extracted from the file name, provided in defaultValues, or are predefined placeholders.
                        • +
                        • Test Your Patterns: Validate your patterns with various file names to ensure they work correctly.
                        • +
                        • Handle Errors Gracefully: Implement error handling for key generation errors to prevent processing failures.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Enumerations

                        ErrorStrategy
                        PostProcessingStrategy

                        Classes

                        FileFlinger

                        Interfaces

                        FileFlingerOptions
                        Pusher
                        diff --git a/docs/modules/_mdf.js_firehose.Plugs.Sink.html b/docs/modules/_mdf.js_firehose.Plugs.Sink.html new file mode 100644 index 00000000..6460b4fc --- /dev/null +++ b/docs/modules/_mdf.js_firehose.Plugs.Sink.html @@ -0,0 +1,4 @@ +Sink | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Interfaces

                        Jet
                        Tap

                        Type Aliases

                        Any
                        diff --git a/docs/modules/_mdf.js_firehose.Plugs.Source.html b/docs/modules/_mdf.js_firehose.Plugs.Source.html new file mode 100644 index 00000000..a69c77ea --- /dev/null +++ b/docs/modules/_mdf.js_firehose.Plugs.Source.html @@ -0,0 +1 @@ +Source | @mdf.js

                        Interfaces

                        CreditsFlow
                        Flow
                        Sequence

                        Type Aliases

                        Any
                        diff --git a/docs/modules/_mdf.js_firehose.Plugs.html b/docs/modules/_mdf.js_firehose.Plugs.html new file mode 100644 index 00000000..f22698aa --- /dev/null +++ b/docs/modules/_mdf.js_firehose.Plugs.html @@ -0,0 +1 @@ +Plugs | @mdf.js

                        Namespaces

                        Sink
                        Source
                        diff --git a/docs/modules/_mdf.js_firehose._internal_.html b/docs/modules/_mdf.js_firehose._internal_.html new file mode 100644 index 00000000..43967f23 --- /dev/null +++ b/docs/modules/_mdf.js_firehose._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_firehose.html b/docs/modules/_mdf.js_firehose.html new file mode 100644 index 00000000..e5a28884 --- /dev/null +++ b/docs/modules/_mdf.js_firehose.html @@ -0,0 +1,279 @@ +@mdf.js/firehose | @mdf.js

                        Module @mdf.js/firehose

                        @mdf.js/firehose

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/firehose

                        +
                        Module designed to facilitate the creation of customized data streaming pipelines.
                        + +
                        + +

                        @mdf.js/firehose is a robust module within the @mdf.js ecosystem, designed to create customized data streaming pipelines. It provides a versatile framework for constructing complex data processing workflows, enabling developers to define custom plugs and strategies for handling diverse data streams.

                        +

                        Before delving into the documentation, it’s essential to understand the core concepts within @mdf.js/firehose:

                        +
                          +
                        • +

                          Plugs: Plugs act as the endpoints of data pipelines, responsible for receiving and sending data to or from the pipeline. They adapt the data stream to the requirements of source or destination systems, aligning with each system’s flow needs. Plugs can be categorized as inputs (Source) or outputs (Sink) and vary by flow conditions supported by the connecting systems:

                          +
                            +
                          • Source: +
                              +
                            • Flow: Plugs that allow continuous data entry; this flow can be paused or restarted based on the pipeline’s state. Typical for data streaming systems like message brokers.
                            • +
                            • Sequence: Plugs that enable data entry in a sequential flow, where the pipeline requests data in specified quantities from the plug. Common for data storage systems such as databases.
                            • +
                            • CreditFlow: Continuous flow plugs that require a credit system to receive data. Common in data streaming systems that necessitate authorization to continue receiving data.
                            • +
                            +
                          • +
                          • Sink: +
                              +
                            • Tap: Plugs that process data one unit at a time, meaning the pipeline calls the plug’s write method for a single data instance. Common in systems that do not support bulk operations.
                            • +
                            • Jet: Plugs that handle data in batches, where the pipeline calls the plug’s write method with multiple data instances. Typical for systems enabling bulk processing.
                            • +
                            +
                          • +
                          +
                        • +
                        • +

                          Jobs: Jobs are instances that transport data and metadata through the pipeline. They manage the data flow between plugs, ensuring correct processing and pipeline state maintenance. Source plugs are notified when a job completes, allowing them to “acknowledge” data from the source system. Jobs can carry additional metadata or processing information, which plugs and strategies can utilize to make decisions or perform specific actions.

                          +
                        • +
                        • +

                          Strategies: Strategies provide customizable, type-based functions that define how to transform job-carried data. Strategies can filter, transform, enrich, or aggregate data as required. They can be chained to build complex data processing workflows, allowing developers to create tailored data pipelines.

                          +
                        • +
                        +

                        This entire ecosystem leverages Node.js streams to build high-performance data processing pipelines capable of efficiently handling large volumes of data.

                        +

                        Firehose

                        +

                        Other key features of @mdf.js/firehose include:

                        +
                          +
                        • Error Handling: The module provides a robust error handling mechanism, allowing developers to define custom error handling strategies for different scenarios.
                        • +
                        • Logging: The module supports logging, enabling developers to track the pipeline’s execution and performance.
                        • +
                        • Metrics: The module provides metrics for monitoring pipeline performance, allowing developers to track data processing efficiency and identify bottlenecks. These metrics are offered in the Prometheus format, using the prom-client library.
                        • +
                        • Multi-Threaded Processing: The module supports multi-threaded processing, allowing developers to leverage the full potential of multi-core processors thanks to the @mdf.js/service-registry module.
                        • +
                        • Tooling: The module provides a set of tools to facilitate the creation of custom plugs and strategies, including a plug generator and a strategy generator. These plugs could be based on the @mdf.js providers.
                        • +
                        +
                          +
                        • npm:
                        • +
                        +
                        npm install @mdf.js/firehose
                        +
                        + +
                          +
                        • yarn:
                        • +
                        +
                        yarn add @mdf.js/firehose
                        +
                        + +

                        The Firehose class, along with the Jobs, Plugs, and Strategies classes, are generic classes that allow you to define the types of the data that will be processed by the pipeline. This is useful to ensure that the data is correctly typed throughout the pipeline.

                        +

                        There are four types that you can define in the Firehose class:

                        +
                          +
                        • Type (Type extends string = string): Job type, used as a selector for strategies in job processing.
                        • +
                        • Data (Data = any): Data type that will be processed by the pipeline.
                        • +
                        • CustomHeaders (CustomHeaders extends Record<string, any> = {}): Custom headers, used to pass specific information for job processors.
                        • +
                        • CustomOptions (CustomOptions extends Record<string, any> = {}): Custom options, used to pass specific information for job processors.
                        • +
                        +

                        For example, if you want to create a pipeline that processes data of type MyData, you can define the Firehose class as follows:

                        +
                        import { Firehose } from '@mdf.js/firehose';

                        type MyJobType = 'TypeOne' | 'TypeTwo';
                        type MyJobDataType<T extends MyJobType> = T extends 'TypeOne'
                        ? { fieldOne: string }
                        : T extends 'TypeTwo'
                        ? { fieldTwo: number }
                        : never;
                        type JobHeaders = { headerOne: string };
                        type JobOptions = { optionOne: string };

                        const myFirehose = new Firehose<MyJobType, MyJobDataType<MyJobType>, JobHeaders, JobOptions>('MyFirehose', {
                        sources: [/* Your source plugs */],
                        sinks: [/* Your sink plugs */],
                        strategies: {
                        TypeOne: [/* Your strategies for TypeOne */],
                        TypeTwo: [/* Your strategies for TypeTwo */],
                        },
                        }); +
                        + +

                        As previously mentioned, jobs are instances that transport data and metadata through the pipeline. They manage the data flow between plugs, ensuring correct processing and pipeline state maintenance. Source and Sink plugs do not use Jobs directly to avoid the need to create and manage them manually. Instead, they use the JobRequest, in the case of Source plugs, and the JobObject, in the case of Sink plugs.

                        +

                        Both of them can be typed using the Firehose class that we previously defined. The JobRequest and JobObject types are defined as follows:

                        +
                          +
                        • JobRequest is an object that contains the data and metadata that the Source plug needs to create a job.
                        • +
                        +
                        export interface JobRequest<
                        Type extends string = string,
                        Data = unknown,
                        CustomHeaders extends Record<string, any> = Jobs.AnyHeaders,
                        CustomOptions extends Record<string, any> = Jobs.AnyOptions,
                        > {
                        /** Job type identification, used to identify specific job handlers to be applied */
                        type?: Type;
                        /** User job request identifier, defined by the user */
                        jobUserId: string;
                        /** Job payload */
                        data: Data;
                        /** Job meta information, used to pass specific information for job processors */
                        options?: Jobs.Options<CustomHeaders, CustomOptions>;
                        } +
                        + +

                        An example of a JobRequest could be:

                        +
                        const jobRequest: JobRequest<MyJobType, MyJobDataType<MyJobType>, JobHeaders, JobOptions> = {
                        type: 'TypeOne',
                        jobUserId: '1234',
                        data: { fieldOne: 'value' },
                        options: { headers: { headerOne: 'value' }, optionOne: 'value' },
                        }; +
                        + +
                          +
                        • JobObject is an object that contains the data and metadata that the Sink plug needs to process a job.
                        • +
                        +
                        export interface JobObject<
                        Type extends string = string,
                        Data = any,
                        CustomHeaders extends Record<string, any> = Jobs.AnyHeaders,
                        CustomOptions extends Record<string, any> = Jobs.AnyOptions,
                        > extends JobRequest<Type, Data, CustomHeaders, CustomOptions> {
                        /** Job type identification, used to identify specific job handlers to be applied */
                        type: Type;
                        /** Unique job processing identification */
                        uuid: string;
                        /**
                        * Unique user job request identification, generated by UUID V5 standard and based on jobUserId
                        */
                        jobUserUUID: string;
                        /** Job status */
                        status: Jobs.Status;
                        } +
                        + +

                        An example of a JobObject could be:

                        +
                        const jobObject: JobObject<MyJobType, MyJobDataType<MyJobType>, JobHeaders, JobOptions> = {
                        type: 'TypeOne',
                        uuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
                        jobUserId: '1234',
                        jobUserUUID: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
                        data: { fieldOne: 'value' },
                        options: { headers: { headerOne: 'value' }, optionOne: 'value' },
                        status: 'pending',
                        }; +
                        + +
                        +

                        When the Sink plug completes processing a job (using the single method for Tap plugs or the single/multi methods for Jet plugs), the Firehose will notify the Source plug that the job has been processed by calling the method postConsume with the jobUserId as a parameter.

                        +
                        +

                        The first step in building a data streaming pipeline is creating a plug. Plugs act as the endpoints of the pipeline, handling data input and output. To create a plug, you need to create your own class that implements the Source or Sink interface. All the Source and Sink interfaces extend the interface Layer.App.Resource, including some additional methods and events to handle the data flow.

                        +
                          +
                        • +

                          Source

                          +
                            +
                          • Events: +
                              +
                            • data: Event that is emitted when the plug has data to be processed. The emitted data is a JobRequest object.
                            • +
                            +
                          • +
                          • Methods: +
                              +
                            • postConsume(jobId: string): Promise<string | undefined>: Method that is called when the plug has processed a job. The parameter is the jobUserId of the processed job. This method must return a promise that resolves to the same jobUserId of the processed job or undefined. If undefined is resolved, the Firehose will understand that the job has not been found in the source plug due to an error. This is an important method to implement to ensure that the pipeline is working correctly by cleaning from the source (if needed) when the job has been processed.
                            • +
                            +
                          • +
                          • Types of Source Plugs: +
                              +
                            • Flow: +
                                +
                              • init(): void: Method that is called when the plug is initialized and indicates that the plug can start emitting data.
                              • +
                              • pause(): void: Method that is called when the plug is paused and indicates that the plug should stop emitting data.
                              • +
                              +
                            • +
                            • Sequence: +
                                +
                              • ingestData(size: number): Promise<JobRequest | JobRequest[]>: Method that is called when the plug needs to ingest data. The parameter is the quantity of data that the plug needs to ingest. This method must return a promise that resolves to a single JobRequest object or an array of JobRequest objects. It's not necessary to resolve the exact quantity of data requested, but the plug must resolve at least one JobRequest object. If the plug doesn't have more data to ingest, it must block the promise until it has more data to ingest.
                              • +
                              +
                            • +
                            • CreditFlow: +
                                +
                              • addCredits(credits: number): Promise<number>: Method that is called to add credits to the plug. The parameter is the quantity of credits to add. This method must return a promise that resolves when the credits have been added to the plug, returning the number of credits that the plug has. Each emitted data will consume one credit. If the plug doesn't have credits, it must wait until addCredits is called to emit more data.
                              • +
                              +
                            • +
                            +
                          • +
                          +
                        • +
                        • +

                          Sink

                          +
                            +
                          • Methods: +
                              +
                            • Tap: +
                                +
                              • single(job: JobObject): Promise<void>: Method that is called to process a single job.
                              • +
                              +
                            • +
                            • Jet: +
                                +
                              • single(job: JobObject): Promise<void>: Method that is called to process a single job.
                              • +
                              • multi(jobs: JobObject[]): Promise<void>: Method that is called to process multiple jobs.
                              • +
                              +
                            • +
                            +
                          • +
                          +
                        • +
                        +

                        As a Layer.App.Resource, the Plugs should include health information in the checks and status properties. The status property should be pass if the plug is healthy and fail if the plug is not healthy. The checks property should include an array of checks that the plug must pass to be considered healthy. If you use the @mdf.js framework to create your plug, you can combine the provider health information with the plug health information.

                        +
                        import { Plugs, JobRequest } from '@mdf.js/firehose';
                        import { EventEmitter } from 'events';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        /** Class that implements the Plugs.Source.Flow interface */
                        class MySourcePlug extends EventEmitter implements Plugs.Source.Flow {
                        /** Constructor */
                        constructor() {
                        super();
                        }
                        /** Method that is called when the firehose has processed a job */
                        public postConsume(jobId: string): Promise<string | undefined> {
                        // Implementation
                        }
                        /** Method that is called when the plug is initialized and indicates that the plug can start emitting data */
                        public init(): void {
                        // Implementation
                        }
                        /** Method that is called when the plug is paused and indicates that the plug should stop emitting data */
                        public pause(): void {
                        // Implementation
                        }
                        /** Start the plug and the underlying provider */
                        public async start(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider */
                        public async stop(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Implementation
                        }
                        /** Prometheus registry to store the metrics of the plug */
                        public get metrics(): Registry {
                        // Implementation
                        }
                        /** Plug health status */
                        public get status(): Health.Status {
                        // Implementation
                        }
                        /** Plug health checks */
                        public get checks(): Health.Checks {
                        // Implementation
                        }
                        } +
                        + +
                        import { Plugs, JobRequest } from '@mdf.js/firehose';
                        import { EventEmitter } from 'events';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        /** Class that implements the Plugs.Source.Sequence interface */
                        class MySourcePlug extends EventEmitter implements Plugs.Source.Sequence {
                        /** Constructor */
                        constructor() {
                        super();
                        }
                        /** Method that is called when the firehose has processed a job */
                        public postConsume(jobId: string): Promise<string | undefined> {
                        // Implementation
                        }
                        /** Method that is called when the plug needs to ingest data */
                        public ingestData(size: number): Promise<JobRequest | JobRequest[]> {
                        // Implementation
                        }
                        /** Start the plug and the underlying provider */
                        public async start(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider */
                        public async stop(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Implementation
                        }
                        /** Prometheus registry to store the metrics of the plug */
                        public get metrics(): Registry {
                        // Implementation
                        }
                        /** Plug health status */
                        public get status(): Health.Status {
                        // Implementation
                        }
                        /** Plug health checks */
                        public get checks(): Health.Checks {
                        // Implementation
                        }
                        } +
                        + +
                        import { Plugs, JobRequest } from '@mdf.js/firehose';
                        import { EventEmitter } from 'events';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        /** Class that implements the Plugs.Source.CreditFlow interface */
                        class MySourcePlug extends EventEmitter implements Plugs.Source.CreditFlow {
                        /** Constructor */
                        constructor() {
                        super();
                        }
                        /** Method that is called when the firehose has processed a job */
                        public postConsume(jobId: string): Promise<string | undefined> {
                        // Implementation
                        }
                        /** Method that is called to add credits to the plug */
                        public addCredits(credits: number): Promise<number> {
                        // Implementation
                        }
                        /** Start the plug and the underlying provider */
                        public async start(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider */
                        public async stop(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Implementation
                        }
                        /** Prometheus registry to store the metrics of the plug */
                        public get metrics(): Registry {
                        // Implementation
                        }
                        /** Plug health status */
                        public get status(): Health.Status {
                        // Implementation
                        }
                        /** Plug health checks */
                        public get checks(): Health.Checks {
                        // Implementation
                        }
                        } +
                        + +
                        import { Plugs, JobRequest } from '@mdf.js/firehose';
                        import { EventEmitter } from 'events';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        /** Class that implements the Plugs.Sink.Tap interface */
                        class MySinkPlug extends EventEmitter implements Plugs.Sink.Tap {
                        /** Constructor */
                        constructor() {
                        super();
                        }
                        /** Method that is called to process a single job */
                        public single(job: JobObject): Promise<void> {
                        // Implementation
                        }
                        /** Start the plug and the underlying provider */
                        public async start(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider */
                        public async stop(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Implementation
                        }
                        /** Prometheus registry to store the metrics of the plug */
                        public get metrics(): Registry {
                        // Implementation
                        }
                        /** Plug health status */
                        public get status(): Health.Status {
                        // Implementation
                        }
                        /** Plug health checks */
                        public get checks(): Health.Checks {
                        // Implementation
                        }
                        } +
                        + +
                        import { Plugs, JobRequest } from '@mdf.js/firehose';
                        import { EventEmitter } from 'events';
                        import { Registry } from 'prom-client';
                        import { Health } from '@mdf.js/core';

                        /** Class that implements the Plugs.Sink.Jet interface */
                        class MySinkPlug extends EventEmitter implements Plugs.Sink.Jet {
                        /** Constructor */
                        constructor() {
                        super();
                        }
                        /** Method that is called to process a single job */
                        public single(job: JobObject): Promise<void> {
                        // Implementation
                        }
                        /** Method that is called to process multiple jobs */
                        public multi(jobs: JobObject[]): Promise<void> {
                        // Implementation
                        }
                        /** Start the plug and the underlying provider */
                        public async start(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider */
                        public async stop(): Promise<void> {
                        // Implementation
                        }
                        /** Stop the plug and the underlying provider and clean the resources */
                        public async close(): Promise<void> {
                        // Implementation
                        }
                        /** Prometheus registry to store the metrics of the plug */
                        public get metrics(): Registry {
                        // Implementation
                        }
                        /** Plug health status */
                        public get status(): Health.Status {
                        // Implementation
                        }
                        /** Plug health checks */
                        public get checks(): Health.Checks {
                        // Implementation
                        }
                        } +
                        + +

                        Strategies provide customizable, type-based functions that define how to transform job-carried data. Strategies can filter, transform, enrich, or aggregate data as required. They can be chained to build complex data processing workflows, allowing developers to create tailored data pipelines.

                        +

                        To create a strategy, you need to create a class that implements the Strategy interface. The Strategy interface includes the following methods and properties:

                        +
                          +
                        • do(process: JobHandler): Promise<JobHandler>: Method that is called to process a job. The parameter is the JobHandler to process. This method must return a promise that resolves to a JobHandler with the processed data.
                        • +
                        • name: string: Strategy name, used to identify the strategy in the pipeline.
                        • +
                        +
                        import { Jobs } from '@mdf.js/core';

                        /** Class that implements the Strategy interface */
                        class MyStrategy implements Jobs.Strategy {
                        /** Strategy name */
                        public get name(): string {
                        return 'MyStrategy';
                        }
                        /** Method that is called to process a job */
                        public async do(process: Jobs.JobHandler): Promise<Jobs.JobHandler> {
                        // Process the job
                        return process;
                        }
                        } +
                        + +

                        The Firehose class extends EventEmitter and emits several events that you can listen to:

                        +
                          +
                        • +

                          error: Emitted when the component detects an error.

                          +
                          firehose.on('error', (error) => {
                          console.error('An error occurred:', error);
                          }); +
                          + +
                        • +
                        • +

                          status: Emitted when the component's status changes.

                          +
                          firehose.on('status', (status) => {
                          console.log('Firehose status:', status);
                          }); +
                          + +
                        • +
                        • +

                          job: Emitted when a new job is received from a source.

                          +
                          firehose.on('job', (job) => {
                          console.log('New job received:', job);
                          }); +
                          + +
                        • +
                        • +

                          done: Emitted when a job has ended, either due to completion or failure.

                          +
                          firehose.on('done', (uuid, result, error) => {
                          if (error) {
                          console.error(`Job ${uuid} failed with error:`, error);
                          } else {
                          console.log(`Job ${uuid} completed with result:`, result);
                          }
                          }); +
                          + +
                        • +
                        • +

                          hold: Emitted when the engine is paused due to inactivity.

                          +
                          firehose.on('hold', () => {
                          console.warn('Firehose is on hold due to inactivity.');
                          }); +
                          + +
                        • +
                        +

                        To instantiate a Firehose, you need to provide a name and options that include the sources, sinks, and optionally, strategies, retry options, buffer size, etc.

                        +
                        import { Firehose } from '@mdf.js/firehose';

                        const firehose = new Firehose('MyFirehose', {
                        sources: [/* Your source plugs */],
                        sinks: [/* Your sink plugs */],
                        strategies: {
                        TypeOne: [/* Strategies for TypeOne */],
                        TypeTwo: [/* Strategies for TypeTwo */],
                        },
                        retryOptions: /* Your retry options */,
                        bufferSize: 100,
                        atLeastOne: true,
                        logger: /* Your logger instance */,
                        maxInactivityTime: 60000,
                        }); +
                        + +

                        To manage the lifecycle of the Firehose, you can use the following methods:

                        +
                          +
                        • start(): Promise<void>: Starts the firehose, initializing all the sources and sinks, and begins processing jobs.
                        • +
                        • stop(): Promise<void>: Stops the firehose, gracefully shutting down all the sources and sinks.
                        • +
                        • close(): Promise<void>: Stops the firehose and cleans up all resources.
                        • +
                        • restart(): Promise<void>: Restarts the firehose, re-initializing all components.
                        • +
                        +

                        Example:

                        +
                        await firehose.start();
                        // Firehose is now running

                        // Later, when you need to stop it
                        await firehose.stop(); +
                        + +

                        The Firehose class includes a Prometheus registry to store the metrics of the pipeline. As you can see in the previous examples, new metrics can be added to the plug classes. The Firehose instance will collect all the metrics of the plugs and will expose them in the metrics property of the Firehose instance.

                        +

                        The default metrics included in the Firehose instance are:

                        +
                          +
                        • api_all_job_processed_total: The total number of all jobs processed, with the label type.
                        • +
                        • api_all_errors_job_processing_total: The total number of errors processing jobs, with the label type.
                        • +
                        • api_all_job_in_processing_total: Number of jobs currently processing (no response yet), with the label type.
                        • +
                        • api_publishing_job_duration_milliseconds: Firehose jobs duration, with the label type.
                        • +
                        • api_publishing_throughput: Firehose throughput in bytes, with the label type.
                        • +
                        +

                        On the other side, Plugs should include health information in the checks and status properties. This information will be grouped with the Firehose health information. The status property should be pass if all the checks are passing and fail if any check is failing.

                        +

                        The health information can be classified into two types: stream status (number of pending jobs in the pipeline) and recent plug operations.

                        +

                        You can access the health status and checks through the status and checks properties:

                        +
                        const firehoseStatus = firehose.status;
                        const firehoseChecks = firehose.checks; +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Plugs

                        Classes

                        Firehose

                        Interfaces

                        FirehoseOptions
                        PostConsumeOptions

                        Type Aliases

                        JobEventHandler
                        diff --git a/docs/modules/_mdf.js_http-client-provider.HTTP.html b/docs/modules/_mdf.js_http-client-provider.HTTP.html new file mode 100644 index 00000000..18e33073 --- /dev/null +++ b/docs/modules/_mdf.js_http-client-provider.HTTP.html @@ -0,0 +1,4 @@ +HTTP | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_http-client-provider.html b/docs/modules/_mdf.js_http-client-provider.html new file mode 100644 index 00000000..093b0179 --- /dev/null +++ b/docs/modules/_mdf.js_http-client-provider.html @@ -0,0 +1,92 @@ +@mdf.js/http-client-provider | @mdf.js

                        Module @mdf.js/http-client-provider

                        @mdf.js/http-client-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/http-client-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        HTTP client provider for @mdf.js based on axios.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/http-client-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/http-client-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        The provider implemented in this module wraps the axios client.

                        +
                        import { HTTP } from '@mdf.js/http-client-provider';

                        const client = HTTP.Factory.create({
                        name: `myHTTPClientProvider`,
                        config: {
                        requestConfig?: {...}, // a CreateAxiosDefaults object from axios
                        httpAgentOptions: {...}, // an http AgentOptions object from Node.js
                        httpsAgentOptions: {...}, // an https AgentOptions object from Node.js
                        },
                        logger: myLoggerInstance,
                        useEnvironment: true,
                        }); +
                        + +
                          +
                        • +

                          Defaults:

                          +
                          {}
                          +
                          + +
                        • +
                        • +

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          +
                          {
                          requestConfig: {
                          baseURL: process.env['CONFIG_HTTP_CLIENT_BASE_URL'],
                          timeout: process.env['CONFIG_HTTP_CLIENT_TIMEOUT'],
                          auth: { // Only if username and password are set
                          username: process.env['CONFIG_HTTP_CLIENT_AUTH_USERNAME'],
                          password: process.env['CONFIG_HTTP_CLIENT_AUTH_PASSWORD'],
                          },
                          },
                          httpAgentOptions: {
                          keepAlive: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE'],
                          keepAliveInitialDelay: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY'],
                          keepAliveMsecs: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS'],
                          maxSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS'],
                          maxTotalSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL'],
                          maxFreeSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE'],
                          },
                          httpsAgentOptions: {
                          keepAlive: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE'],
                          keepAliveInitialDelay: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY'],
                          keepAliveMsecs: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS'],
                          maxSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS'],
                          maxTotalSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL'],
                          maxFreeSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE'],
                          rejectUnauthorized: process.env['CONFIG_HTTP_CLIENT_REJECT_UNAUTHORIZED'],
                          ca: process.env['CONFIG_HTTP_CLIENT_CA_PATH'],
                          cert: process.env['CONFIG_HTTP_CLIENT_CLIENT_CERT_PATH'],
                          key: process.env['CONFIG_HTTP_CLIENT_CLIENT_KEY_PATH'],
                          },
                          } +
                          + +
                        • +
                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Due to the nature of the HTTP client, the status check is not implemented. The provider is always in running state. +
                            +
                          • observedValue: running.
                          • +
                          • status: pass.
                          • +
                          • output: undefined.
                          • +
                          • componentType: service.
                          • +
                          +
                        • +
                        +
                        {
                        "[mdf-http-client:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "service",
                        "output": undefined,
                        },
                        ],
                        } +
                        + +
                          +
                        • CONFIG_HTTP_CLIENT_BASE_URL (default: undefined): Base URL for the HTTP client requests.
                        • +
                        • CONFIG_HTTP_CLIENT_TIMEOUT (default: undefined): Time in milliseconds before the request is considered a timeout.
                        • +
                        • CONFIG_HTTP_CLIENT_AUTH_USERNAME (default: undefined): Username for the HTTP client authentication, if username is set, password must be set too.
                        • +
                        • CONFIG_HTTP_CLIENT_AUTH_PASSWORD (default: undefined): Password for the HTTP client authentication if password is set, username must be set too.
                        • +
                        • CONFIG_HTTP_CLIENT_KEEPALIVE (default: false): Keep sockets around in a pool to be used by other requests in the future.
                        • +
                        • CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY (default: undefined): Time in milliseconds before the keep alive feature is enabled.
                        • +
                        • CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS (default: 1000): When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. Only relevant if keepAlive is set to true.
                        • +
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS (default: Infinity): Maximum number of sockets to allow per host. Default for Node 0.10 is 5, default for Node 0.12 is Infinity.
                        • +
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL (default: Infinity): Maximum number of sockets allowed for all hosts in total. Each request will use a new socket until the maximum is reached. Default: Infinity.
                        • +
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE (default: 256): Maximum number of sockets to leave open in a free state. Only relevant if keepAlive is set to true.
                        • +
                        • CONFIG_HTTP_CLIENT_REJECT_UNAUTHORIZED (default: true): Reject unauthorized TLS certificates.
                        • +
                        • CONFIG_HTTP_CLIENT_CA_PATH (default: undefined): Path to the CA certificate file.
                        • +
                        • CONFIG_HTTP_CLIENT_CLIENT_CERT_PATH (default: undefined): Path to the client certificate file.
                        • +
                        • CONFIG_HTTP_CLIENT_CLIENT_KEY_PATH (default: undefined): Path to the client key file.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        HTTP
                        diff --git a/docs/modules/_mdf.js_http-server-provider.HTTP.html b/docs/modules/_mdf.js_http-server-provider.HTTP.html new file mode 100644 index 00000000..63b700c7 --- /dev/null +++ b/docs/modules/_mdf.js_http-server-provider.HTTP.html @@ -0,0 +1,4 @@ +HTTP | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_http-server-provider.html b/docs/modules/_mdf.js_http-server-provider.html new file mode 100644 index 00000000..a6afc6f2 --- /dev/null +++ b/docs/modules/_mdf.js_http-server-provider.html @@ -0,0 +1,80 @@ +@mdf.js/http-server-provider | @mdf.js

                        Module @mdf.js/http-server-provider

                        @mdf.js/http-server-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/http-server-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        HTTP server provider for @mdf.js based on express.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/http-server-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/http-server-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        The provider implemented in this module wraps an HTTP server based on express to provide a simple HTTP server.

                        +
                        import { HTTP } from '@mdf.js/http-server-provider';

                        const client = HTTP.Factory.create({
                        name: `myHTTPServerProvider`,
                        config: {
                        /** Port for the HTTP server */
                        port: 8080,
                        /** Host for the HTTP server */
                        host: 'localhost',
                        /** Express app configuration */
                        app: Express,
                        },
                        logger: myLoggerInstance,
                        useEnvironment: true,
                        }); +
                        + +
                          +
                        • +

                          Defaults:

                          +
                          {
                          port: 8080,
                          host: 'localhost',
                          app: /* Default static web */,
                          } +
                          + +
                        • +
                        • +

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          +
                          {
                          port: process.env['CONFIG_SERVER_PORT'],
                          host: process.env['CONFIG_SERVER_HOST'],
                          } +
                          + +
                        • +
                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Due to the nature of the HTTP server, the status could be running if the server has been started properly, stopped if the server has been stopped or is not initialized, or error if the server could not be started. +
                            +
                          • observedValue: running if the server is running, stopped if the server is stopped, or error if the server could not be started.
                          • +
                          • status: pass if the server is running, fail could not be started or warn if the server is stopped.
                          • +
                          • output: In case of error state (status fail), the error message is shown.
                          • +
                          • componentType: service.
                          • +
                          +
                        • +
                        +
                        {
                        "[mdf-http-server:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "service",
                        "output": undefined,
                        },
                        ],
                        } +
                        + +
                          +
                        • CONFIG_SERVER_PORT (default: 8080): Port for the HTTP server.
                        • +
                        • CONFIG_SERVER_HOST (default: localhost): Host for the HTTP server.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        HTTP
                        diff --git a/docs/modules/_mdf.js_jsonl-archiver.JSONLArchiver.html b/docs/modules/_mdf.js_jsonl-archiver.JSONLArchiver.html new file mode 100644 index 00000000..09b6ee4c --- /dev/null +++ b/docs/modules/_mdf.js_jsonl-archiver.JSONLArchiver.html @@ -0,0 +1 @@ +JSONLArchiver | @mdf.js

                        Classes - Provider

                        Port

                        Type Aliases

                        Config
                        Provider

                        Variables

                        Factory

                        References

                        Client → ArchiverManager
                        diff --git a/docs/modules/_mdf.js_jsonl-archiver._internal_.html b/docs/modules/_mdf.js_jsonl-archiver._internal_.html new file mode 100644 index 00000000..e47833d9 --- /dev/null +++ b/docs/modules/_mdf.js_jsonl-archiver._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_jsonl-archiver.html b/docs/modules/_mdf.js_jsonl-archiver.html new file mode 100644 index 00000000..b9d6a099 --- /dev/null +++ b/docs/modules/_mdf.js_jsonl-archiver.html @@ -0,0 +1,111 @@ +@mdf.js/jsonl-archiver | @mdf.js

                        Module @mdf.js/jsonl-archiver

                        @mdf.js/jsonl-archiver-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/jsonl-archiver-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        @mdf.js/jsonl-archiver-provider is a tool designed for managing the storage of jsonl files in Node.js applications. It allows to append data to multiple files, maintaining the append and rotation processes independent for each file.

                        +

                        Abstract the management of jsonl files in Node.js applications, providing a simple and efficient way to store data in a jsonl format. This module is designed to be used in the @mdf.js environment, but it can be used in any Node.js application. Some examples of use cases are:

                        +
                          +
                        • Store logs in a jsonl format
                        • +
                        • Store data in a jsonl format
                        • +
                        +
                          +
                        • Append data to files (as per jsonl format, each line is a json string), with: +
                            +
                          • Automatic file rotation by size, lines or time, moving the file to an archive folder.
                          • +
                          • Automatic destination file selection by json properties.
                          • +
                          • Automatic skip data to be appended by json properties.
                          • +
                          +
                        • +
                        +

                        Using npm:

                        +
                        npm install @mdf.js/jsonl-archive-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/jsonl-archive-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        This module is developed as a @mdf.js Provider so that it can be used easily in any application, both in the @mdf.js environment and in any other Node.js application.

                        +

                        In order to use this module, your should use the Factory exposed and create an instance using the create method:

                        +
                        import { Factory } from '@mdf.js/jsonl-file-store-provider';

                        const default = Factory.create(); // Create a new instance with default options

                        const custom = Factory.create({
                        config: {...} // Custom options
                        name: 'custom' // Custom name
                        useEnvironment: true // Use environment variables
                        logger: myLoggerInstance // Custom logger
                        }); +
                        + +

                        The configuration options (config) are the following:

                        +
                          +
                        • +

                          Defaults:

                          +
                          export interface ArchiveOptions {
                          separator?: string;
                          propertyData?: string;
                          propertyFileName?: string;
                          propertySkip?: string;
                          propertySkipValue?: string | number | boolean;
                          defaultBaseFilename?: string;
                          workingFolderPath: string;
                          archiveFolderPath: string;
                          createFolders: boolean;
                          inactiveTimeout?: number;
                          fileEncoding: BufferEncoding;
                          rotationInterval?: number;
                          rotationSize?: number;
                          rotationLines?: number;
                          retryOptions?: RetryOptions;
                          logger?: LoggerInstance;
                          } +
                          + +

                          Where each property has the next meaning:

                          +
                            +
                          • separator (default: \n): Separator to use when writing the data to the file.
                          • +
                          • propertyData (default: undefined): If set, this property will be used to store the data in the file, it could be a nested property in the data object expressed as a dot separated string.
                          • +
                          • propertyFileName (default: undefined): If set, this property will be used as the filename, it could be a nested property in the data object expressed as a dot separated string.
                          • +
                          • propertySkip (default: undefined): If set, this property will be used to skip the data, it could be a nested property in the data object expressed as a dot separated string.
                          • +
                          • propertySkipValue (default: undefined): If set, this value will be used to skip the data, it could be a string, number or boolean. If value is not set, but propertySkip is set, a not falsy value will be used to skip the data, this means that any value that is not false, 0 or '' will be used to skip the data.
                          • +
                          • defaultBaseFilename (default: 'file'): Base filename for the files.
                          • +
                          • workingFolderPath (default: './data/working'): Path to the folder where the working files are stored.
                          • +
                          • archiveFolderPath (default: './data/archive'): Path to the folder where the closed files are stored.
                          • +
                          • createFolders (default: true): If true, it will create the folders if they don't exist.
                          • +
                          • inactiveTimeout (default: undefined): Maximum inactivity time in milliseconds before a handler is cleaned up.
                          • +
                          • fileEncoding (default: 'utf-8'): Encoding to use when writing to files.
                          • +
                          • rotationInterval (default: 600000): Interval in milliseconds to rotate the file.
                          • +
                          • rotationSize (default: 10485760): Max size of the file before rotating it.
                          • +
                          • rotationLines (default: 10000): Max number of lines before rotating the file.
                          • +
                          • retryOptions (default: { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }): Retry options for the file handler operations. Check the @mdf.js/utils module for more information.
                          • +
                          • logger (default: undefined): Logger instance to use. Check the @mdf.js/logger module for more information.
                          • +
                          +
                        • +
                        • +

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          +
                          { 
                          workingFolderPath: process.env['CONFIG_JSONL_ARCHIVER_WORKING_FOLDER_PATH'],
                          archiveFolderPath: process.env['CONFIG_JSONL_ARCHIVER_ARCHIVE_FOLDER_PATH'],
                          fileEncoding: process.env['CONFIG_JSONL_ARCHIVER_FILE_ENCODING'],
                          createFolders: process.env['CONFIG_JSONL_ARCHIVER_CREATE_FOLDERS'], /* boolean */
                          rotationInterval: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_INTERVAL'], /* number */
                          rotationSize: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_SIZE'], /* number */
                          rotationLines: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_LINES'], /* number */
                          } +
                          + +
                        • +
                        +
                          +
                        • CONFIG_JSONL_ARCHIVER_WORKING_FOLDER_PATH (default: './data/working'): Path to the folder where the open files are stored
                        • +
                        • CONFIG_JSONL_ARCHIVER_ARCHIVE_FOLDER_PATH (default: './data/archive'): Path to the folder where the closed files are stored
                        • +
                        • CONFIG_JSONL_ARCHIVER_FILE_ENCODING (default: 'utf-8'): File encoding
                        • +
                        • CONFIG_JSONL_ARCHIVER_CREATE_FOLDERS (default: true): Create folders if they do not exist
                        • +
                        • CONFIG_JSONL_ARCHIVER_ROTATION_INTERVAL (default: 600000): Interval in milliseconds to rotate the file
                        • +
                        • CONFIG_JSONL_ARCHIVER_ROTATION_SIZE (default: 10485760): Max size of the file before rotating it
                        • +
                        • CONFIG_JSONL_ARCHIVER_ROTATION_LINES (default: 10000): Max number of lines before rotating the file
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        JSONLArchiver

                        Classes

                        ArchiverManager

                        Interfaces

                        AppendResult
                        ArchiveOptions
                        FileStats
                        diff --git a/docs/modules/_mdf.js_kafka-provider.Consumer.html b/docs/modules/_mdf.js_kafka-provider.Consumer.html new file mode 100644 index 00000000..a6a9740c --- /dev/null +++ b/docs/modules/_mdf.js_kafka-provider.Consumer.html @@ -0,0 +1,4 @@ +Consumer | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_kafka-provider.Producer.html b/docs/modules/_mdf.js_kafka-provider.Producer.html new file mode 100644 index 00000000..7fdcc255 --- /dev/null +++ b/docs/modules/_mdf.js_kafka-provider.Producer.html @@ -0,0 +1 @@ +Producer | @mdf.js

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_kafka-provider._internal_.html b/docs/modules/_mdf.js_kafka-provider._internal_.html new file mode 100644 index 00000000..0ebdf9d1 --- /dev/null +++ b/docs/modules/_mdf.js_kafka-provider._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js

                        Classes - Other

                        Client
                        Consumer
                        Producer

                        Classes - Provider

                        BasePort

                        Interfaces

                        BaseConfig

                        Type Aliases

                        SystemStatus
                        diff --git a/docs/modules/_mdf.js_kafka-provider.html b/docs/modules/_mdf.js_kafka-provider.html new file mode 100644 index 00000000..cb49165b --- /dev/null +++ b/docs/modules/_mdf.js_kafka-provider.html @@ -0,0 +1,111 @@ +@mdf.js/kafka-provider | @mdf.js

                        Module @mdf.js/kafka-provider

                        @mdf.js/kafka-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/kafka-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        Kafka provider for @mdf.js based on kafkajs.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/kafka-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/kafka-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the status of the kafka nodes using the admin client of the KafkaJS library, performing several requests about the status of the nodes and groups. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based on the response, or not, to admin client requests. error if there is errors during the requests, running if the requests are successful, and stopped if the instance has been stopped or not initialized.
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          • output: Shows the error message in case of error state (status fail).
                          • +
                          +
                        • +
                        • topics: Checks the topics available in the Kafka connection +
                            +
                          • observedValue: List of topics available in the Kafka connection.
                          • +
                          • observedUnit: topics.
                          • +
                          • status: pass if the topics are available, fail in other cases.
                          • +
                          • output: No topics available if the topics are not available.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_KAFKA_PRODUCER__METADATA_MAX_AGE (default: 300000): Maximum time in ms that the producer will wait for metadata
                        • +
                        • CONFIG_KAFKA_PRODUCER__ALLOW_AUTO_TOPIC_CREATION (default: true): Allow auto topic creation
                        • +
                        • CONFIG_KAFKA_PRODUCER__TRANSACTION_TIMEOUT (default: 60000): Transaction timeout in ms
                        • +
                        • CONFIG_KAFKA_PRODUCER__IDEMPOTENT (default: false): Idempotent producer
                        • +
                        • CONFIG_KAFKA_PRODUCER__TRANSACTIONAL_ID (default: undefined): Transactional id
                        • +
                        • CONFIG_KAFKA_PRODUCER__MAX_IN_FLIGHT_REQUEST (default: undefined): Maximum number of in-flight requests
                        • +
                        • CONFIG_KAFKA_PRODUCER__RETRY__MAX_RETRY_TIME (default: 300000): Maximum time in ms that the producer will wait for metadata
                        • +
                        • CONFIG_KAFKA_PRODUCER__RETRY__INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • +
                        • CONFIG_KAFKA_PRODUCER__RETRY__FACTOR (default: 0.2): A multiplier to apply to the retry time
                        • +
                        • CONFIG_KAFKA_PRODUCER__RETRY__MULTIPLIER (default: 2): A multiplier to apply to the retry time
                        • +
                        • CONFIG_KAFKA_PRODUCER__RETRY__RETRIES (default: 5): Maximum number of retries per call
                        • +
                        • CONFIG_KAFKA_CONSUMER__GROUP_ID (default: 'hostname()'): Consumer group id
                        • +
                        • CONFIG_KAFKA_CONSUMER__SESSION_TIMEOUT (default: 30000): The timeout used to detect consumer failures when using Kafka's group management facility. The consumer sends periodic heartbeats to indicate its liveness to the broker. If no heartbeats are received by the broker before the expiration of this session timeout, then the broker will remove this consumer from the group and initiate a rebalance.
                        • +
                        • CONFIG_KAFKA_CONSUMER__REBALANCE_TIMEOUT (default: 60000): The maximum time that the coordinator will wait for each member to rejoin when rebalancing the group.
                        • +
                        • CONFIG_KAFKA_CONSUMER__HEARTBEAT_INTERVAL (default: 3000): The expected time between heartbeats to the consumer coordinator when using Kafka's group management facility. Heartbeats are used to ensure that the consumer's session stays active and to facilitate rebalancing when new consumers join or leave the group. The value must be set lower than sessionTimeout, but typically should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances.
                        • +
                        • CONFIG_KAFKA_CONSUMER__METADATA_MAX_AGE (default: 300000): The period of time in milliseconds after which we force a refresh of metadata even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions.
                        • +
                        • CONFIG_KAFKA_CONSUMER__ALLOW_AUTO_TOPIC_CREATION (default: true): Allow automatic topic creation on the broker when subscribing to or assigning non-existing topics.
                        • +
                        • CONFIG_KAFKA_CONSUMER__MAX_BYTES_PER_PARTITION (default: 1048576): The maximum amount of data per-partition the server will return.
                        • +
                        • CONFIG_KAFKA_CONSUMER__MIN_BYTES (default: 1): Minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait until some is available.
                        • +
                        • CONFIG_KAFKA_CONSUMER__MAX_BYTES (default: 10485760): The maximum amount of data the server should return for a fetch request.
                        • +
                        • CONFIG_KAFKA_CONSUMER_MAX_WAIT_TIME_IN_MS (default: 5000): The maximum amount of time the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy minBytes.
                        • +
                        • CONFIG_KAFKA_CONSUMER__RETRY__MAX_RETRY_TIME (default: 30000): Maximum time in milliseconds to wait for a successful retry
                        • +
                        • CONFIG_KAFKA_CONSUMER__RETRY__INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • +
                        • CONFIG_KAFKA_CONSUMER__RETRY__FACTOR (default: 0.2): A multiplier to apply to the retry time
                        • +
                        • CONFIG_KAFKA_CONSUMER__RETRY__MULTIPLIER (default: 2): A multiplier to apply to the retry time
                        • +
                        • CONFIG_KAFKA_CONSUMER__RETRY__RETRIES (default: 5): Maximum number of retries per call
                        • +
                        • CONFIG_KAFKA_CONSUMER__READ_UNCOMMITTED (default: false): Whether to read uncommitted messages
                        • +
                        • CONFIG_KAFKA_CONSUMER__MAX_IN_FLIGHT_REQUEST (default: undefined): Maximum number of in-flight requests
                        • +
                        • CONFIG_KAFKA_CONSUMER__RACK_ID (default: undefined): The consumer will only be assigned partitions from the leader of the partition to which it is assigned.
                        • +
                        • CONFIG_KAFKA_LOG_LEVEL (default: `error`): Define the log level for the kafka provider, possible values are: - error - warn - info - debug - trace
                        • +
                        • CONFIG_KAFKA_CLIENT__CLIENT_ID (default: hostname): Client identifier
                        • +
                        • CONFIG_KAFKA_CLIENT__BROKERS (default: '127.0.0.1:9092'): Kafka brokers
                        • +
                        • CONFIG_KAFKA_CLIENT__CONNECTION_TIMEOUT (default: 1000): Time in milliseconds to wait for a successful connection
                        • +
                        • CONFIG_KAFKA_CLIENT__AUTHENTICATION_TIMEOUT (default: 1000): Timeout in ms for authentication requests
                        • +
                        • CONFIG_KAFKA_CLIENT__REAUTHENTICATION_THRESHOLD (default: 1000): When periodic reauthentication (connections.max.reauth.ms) is configured on the broker side, reauthenticate when reauthenticationThreshold milliseconds remain of session lifetime.
                        • +
                        • CONFIG_KAFKA_CLIENT__REQUEST_TIMEOUT (default: 30000): Time in milliseconds to wait for a successful request
                        • +
                        • CONFIG_KAFKA_CLIENT__ENFORCE_REQUEST_TIMEOUT (default: true): The request timeout can be disabled by setting this value to false.
                        • +
                        • CONFIG_KAFKA_MAX_RETRY_TIME (default: 30000): Maximum time in milliseconds to wait for a successful retry
                        • +
                        • CONFIG_KAFKA_INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • +
                        • CONFIG_KAFKA_RETRY_FACTOR (default: 0.2): Randomization factor
                        • +
                        • CONFIG_KAFKA_RETRY_MULTIPLIER (default: 2): Exponential factor
                        • +
                        • CONFIG_KAFKA_RETRIES (default: 5): Maximum number of retries per call
                        • +
                        • CONFIG_KAFKA_CLIENT_SSL_ENABLED (default: false): Whether to use SSL
                        • +
                        • CONFIG_KAFKA_CLIENT__SSL__REJECT_UNAUTHORIZED (default: true): Whether to verify the SSL certificate.
                        • +
                        • CONFIG_KAFKA_CLIENT__SSL__SERVER_NAME (default: undefined): Server name for the TLS certificate.
                        • +
                        • CONFIG_KAFKA_CLIENT_SSL_CA_PATH (default: undefined): Path to the CA certificate.
                        • +
                        • CONFIG_KAFKA_CLIENT_SSL_CERT_PATH (default: undefined): Path to the client certificate.
                        • +
                        • CONFIG_KAFKA_CLIENT_SSL_KEY_PATH (default: undefined): Path to the client key.
                        • +
                        • CONFIG_KAFKA_CLIENT__SASL_USERNAME (default: undefined): SASL username
                        • +
                        • CONFIG_KAFKA_CLIENT__SASL_PASSWORD (default: undefined): SASL password
                        • +
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Consumer
                        Producer
                        diff --git a/docs/modules/_mdf.js_logger.html b/docs/modules/_mdf.js_logger.html new file mode 100644 index 00000000..48106187 --- /dev/null +++ b/docs/modules/_mdf.js_logger.html @@ -0,0 +1,150 @@ +@mdf.js/logger | @mdf.js

                        Module @mdf.js/logger

                        @mdf.js/logger

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        @mdf.js/logger

                        +
                        Improved logger management for @mdf.js framework
                        + +
                        + +

                        @mdf.js/logger is a powerful and flexible logging module designed for the @mdf.js framework. It provides enhanced logging capabilities with support for multiple logging levels and transports, including console, file, and Fluentd. This module allows developers to easily integrate robust logging into their applications, enabling better debugging, monitoring, and error tracking.

                        +

                        Logger

                        +

                        Install the @mdf.js/logger module via npm:

                        +
                          +
                        • npm
                        • +
                        +
                        npm install @mdf.js/logger
                        +
                        + +
                          +
                        • yarn
                        • +
                        +
                        yarn add @mdf.js/logger
                        +
                        + +
                          +
                        • Multiple Log Levels: Supports standard log levels (silly, debug, verbose, info, warn, error) for granular control over logging output.
                        • +
                        • Customizable Transports: Supports logging to console, files, and Fluentd.
                        • +
                        • Flexible Configuration: Easily configure logging options to suit your application's needs.
                        • +
                        • Contextual Logging: Support for context and unique identifiers (UUID) to trace logs across different parts of your application.
                        • +
                        • Error Handling: Ability to log errors and crashes with detailed stack traces and metadata.
                        • +
                        • Integration with @mdf.js Framework: Seamless integration with other @mdf.js modules.
                        • +
                        +

                        To use @mdf.js/logger in your project, import the module and create a new logger instance:

                        +
                        import { Logger } from '@mdf.js/logger';

                        const logger = new Logger('my-app'); +
                        + +

                        This creates a new logger with default settings for your application labeled 'my-app'.

                        +

                        The logger instance provides methods for logging at different levels:

                        +
                          +
                        • silly(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • debug(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • verbose(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • info(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • warn(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • error(message: string, uuid?: string, context?: string, ...meta: any[]): void;
                        • +
                        • crash(error: Crash | Boom | Multi, context?: string): void;
                        • +
                        +
                          +
                        • message: A human-readable string message to log.
                        • +
                        • uuid (optional): A unique identifier (UUID) for tracing the log message across different components or requests.
                        • +
                        • context (optional): The context (e.g., class or function name) where the logger is logging.
                        • +
                        • ...meta (optional): Additional metadata or objects to include in the log.
                        • +
                        +

                        Logging an info message without UUID and context:

                        +
                        logger.info('Application started');
                        +
                        + +

                        Logging an error message with UUID and context:

                        +
                        const uuid = '02ef7b85-b88e-4134-b611-4056820cd689';
                        const context = 'UserService';

                        logger.error('User not found', uuid, context, { userId: 'user123' }); +
                        + +

                        To simplify logging with a fixed context and UUID, you can create a wrapped logger using the SetContext function:

                        +
                        import { SetContext } from '@mdf.js/logger';

                        const wrappedLogger = SetContext(logger, 'AuthService', '123e4567-e89b-12d3-a456-426614174000');

                        wrappedLogger.info('User login successful', undefined, undefined, { userId: 'user123' }); +
                        + +

                        In this case, the uuid and context parameters are pre-set, and you can omit them in subsequent log calls.

                        +

                        To log errors or crashes with detailed stack traces and metadata, use the crash method:

                        +
                        import { Crash } from '@mdf.js/crash';

                        try {
                        // Code that may throw an error
                        } catch (error) {
                        const crashError = Crash.from(error);
                        logger.crash(crashError, 'AuthService');
                        } +
                        + +

                        The crash method logs the error at the error level, including the stack trace and additional information.

                        +

                        You can customize the logger by passing a configuration object:

                        +
                        import { Logger, LoggerConfig } from '@mdf.js/logger';

                        const config: LoggerConfig = {
                        console: {
                        enabled: true,
                        level: 'debug',
                        },
                        file: {
                        enabled: true,
                        filename: 'logs/my-app.log',
                        level: 'info',
                        maxFiles: 5,
                        maxsize: 10485760, // 10 MB
                        zippedArchive: true,
                        },
                        fluentd: {
                        enabled: false,
                        // Additional Fluentd configurations for fluent-logger module
                        },
                        };

                        const logger = new Logger('my-app', config); +
                        + +

                        ### **Using DebugLogger**

                        If you prefer using the `debug` module, utilize the `DebugLogger` class:

                        ```typescript
                        import { DebugLogger } from '@mdf.js/logger';

                        const debugLogger = new DebugLogger('my-app');

                        debugLogger.debug('This is a debug message using DebugLogger'); +
                        + +

                        The LoggerConfig interface allows you to configure different transports:

                        +
                        interface LoggerConfig {
                        console?: ConsoleTransportConfig;
                        file?: FileTransportConfig;
                        fluentd?: FluentdTransportConfig;
                        } +
                        + +
                        interface ConsoleTransportConfig {
                        enabled?: boolean; // Default: false
                        level?: LogLevel; // Default: 'info'
                        } +
                        + +
                        interface FileTransportConfig {
                        enabled?: boolean; // Default: false
                        level?: LogLevel; // Default: 'info'
                        filename?: string; // Default: 'logs/mdf-app.log'
                        maxFiles?: number; // Default: 10
                        maxsize?: number; // Default: 10485760 (10 MB)
                        zippedArchive?: boolean; // Default: false
                        json?: boolean; // Default: false
                        } +
                        + +
                        type FluentdTransportConfig = {
                        enabled?: boolean; // Default: false
                        level?: LogLevel; // Default: 'info'
                        // Additional Fluentd-specific options here
                        }; +
                        + +

                        Available log levels are defined by the LogLevel type:

                        +
                        type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly';
                        +
                        + +

                        The logger handles configuration errors gracefully. If there's an error in the provided configuration, the logger defaults to predefined settings and logs the configuration error:

                        +
                        const invalidConfig: LoggerConfig = {
                        console: {
                        enabled: true,
                        level: 'invalid-level' as LogLevel, // This will cause a validation error
                        },
                        };

                        const logger = new Logger('my-app', invalidConfig);

                        // The logger will use default settings and log the configuration error +
                        + +

                        You can check if the logger has encountered any configuration errors:

                        +
                        if (logger.hasError) {
                        console.error('Logger configuration error:', logger.configError);
                        } +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Classes

                        DebugLogger
                        Logger
                        WrapperLogger

                        Interfaces

                        ConsoleTransportConfig
                        FileTransportConfig
                        LoggerConfig
                        LoggerInstance

                        Type Aliases

                        FluentdTransportConfig
                        LoggerFunction
                        LogLevel

                        Variables

                        default
                        LOG_LEVELS

                        Functions

                        SetContext
                        diff --git a/docs/modules/_mdf.js_middlewares._internal_.html b/docs/modules/_mdf.js_middlewares._internal_.html new file mode 100644 index 00000000..a0975615 --- /dev/null +++ b/docs/modules/_mdf.js_middlewares._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_middlewares.html b/docs/modules/_mdf.js_middlewares.html new file mode 100644 index 00000000..6770de0a --- /dev/null +++ b/docs/modules/_mdf.js_middlewares.html @@ -0,0 +1,29 @@ +@mdf.js/middlewares | @mdf.js

                        Module @mdf.js/middlewares

                        @mdf.js

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js

                        +
                        Typescript tools for development
                        + +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Classes

                        Audit
                        AuthZ
                        BodyParser
                        Cache
                        Cors
                        Default
                        ErrorHandler
                        Logger
                        Metrics
                        Multer
                        NoCache
                        RateLimiter
                        RequestId
                        Security

                        Interfaces

                        AuditConfig
                        CacheConfig
                        CorsConfig
                        RateLimitConfig
                        RateLimitEntry

                        Type Aliases

                        AfterRoutesMiddlewares
                        AuditCategory
                        AuthZOptions
                        BeforeRoutesMiddlewares
                        EndpointsMiddlewares
                        Middlewares

                        Variables

                        Middleware
                        diff --git a/docs/modules/_mdf.js_mongo-provider.Mongo.html b/docs/modules/_mdf.js_mongo-provider.Mongo.html new file mode 100644 index 00000000..998b280a --- /dev/null +++ b/docs/modules/_mdf.js_mongo-provider.Mongo.html @@ -0,0 +1,4 @@ +Mongo | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        Collections
                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_mongo-provider.html b/docs/modules/_mdf.js_mongo-provider.html new file mode 100644 index 00000000..a859db18 --- /dev/null +++ b/docs/modules/_mdf.js_mongo-provider.html @@ -0,0 +1,80 @@ +@mdf.js/mongo-provider | @mdf.js

                        Module @mdf.js/mongo-provider

                        @mdf.js/mongo-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        MongoDB Provider for @mdf.js/mongo-provider

                        +
                        Mytra Development Framework - @mdf.js
                        + +
                        + +

                        MongoDB provider for @mdf.js based on mongodb.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/mongo-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/mongo-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the status of the MongoDB nodes using the heartbeat events from the client. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last heartbeat event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last heartbeat event was successful, error if the provider is running and the last heartbeat event was not successful.
                          • +
                          • observedUnit: status.
                          • +
                          • status: fail if the observed value is error, warn if the observed value is stopped, pass in other case.
                          • +
                          +
                        • +
                        • heartbeat: +
                            +
                          • observedValue: failed if the last heartbeat was not successful, heartbeat information if the last heartbeat was successful.
                          • +
                          • observedUnit: heartbeat result.
                          • +
                          • status: fail if the observed value is failed, pass in other case.
                          • +
                          • output: shows the connection identifier and the failure message in case of failed state (status fail). undefined in other case.
                          • +
                          +
                        • +
                        • lastCommand: +
                            +
                          • observedValue: succeeded if the last command executed in the provider was successful, failed if the last command executed in the provider failed.
                          • +
                          • observedUnit: command result.
                          • +
                          • status: pass if the observed value is succeeded, fail if the observed value is failed.
                          • +
                          • output: Shows the command name and the command failure message in case of failed state (status fail). undefined if the observed value is succeeded.
                          • +
                          +
                        • +
                        • lastFailedCommands: +
                            +
                          • observedValue: Shows the last 10 failed commands executed in the provider, each entry shows the date of the command in ISO format, the command name and the failure message.
                          • +
                          • observedUnit: last failed commands.
                          • +
                          • status: pass.
                          • +
                          • output: undefined.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_MONGO_URL (default: `mongodb://127.0.0.1:27017/mdf`): URL for the mongo database
                        • +
                        • CONFIG_MONGO_CA_PATH (default: undefined): Path to the CA for the mongo database
                        • +
                        • CONFIG_MONGO_CERT_PATH (default: undefined): Path to the cert for the mongo database
                        • +
                        • CONFIG_MONGO_KEY_PATH (default: undefined): Path to the key for the mongo database
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        Mongo
                        diff --git a/docs/modules/_mdf.js_mqtt-provider.MQTT.html b/docs/modules/_mdf.js_mqtt-provider.MQTT.html new file mode 100644 index 00000000..7ef10e1d --- /dev/null +++ b/docs/modules/_mdf.js_mqtt-provider.MQTT.html @@ -0,0 +1,4 @@ +MQTT | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_mqtt-provider.html b/docs/modules/_mdf.js_mqtt-provider.html new file mode 100644 index 00000000..3b2babad --- /dev/null +++ b/docs/modules/_mdf.js_mqtt-provider.html @@ -0,0 +1,70 @@ +@mdf.js/mqtt-provider | @mdf.js

                        Module @mdf.js/mqtt-provider

                        @mdf.js/mqtt-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/mqtt-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        MQTT provider for @mdf.js based on mqtt.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/mqtt-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/mqtt-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the ping messages from the server. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last ping event was successful, error if the provider is running and the last ping event was not successful.
                          • +
                          • observedUnit: status.
                          • +
                          • status: fail if the observed value is error, warn if the observed value is stopped, pass in other case.
                          • +
                          +
                        • +
                        • lastError: +
                            +
                          • observedValue: last error message from the provider.
                          • +
                          • observedUnit: Last error.
                          • +
                          • status: pass.
                          • +
                          • output: last error message from the provider.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_MQTT_URL (default: 'mqtt://localhost:1883'): URL of the server
                        • +
                        • CONFIG_MQTT_PROTOCOL (default: 'mqtt'): Protocol to use
                        • +
                        • CONFIG_MQTT_USERNAME (default: undefined): Username
                        • +
                        • CONFIG_MQTT_PASSWORD (default: undefined): Password
                        • +
                        • CONFIG_MQTT_CLIENT_ID (default: 'mqtt-client'): Client ID
                        • +
                        • CONFIG_MQTT_KEEPALIVE (default: 60): Keepalive in seconds
                        • +
                        • CONFIG_MQTT_CLIENT_CA_PATH (default: undefined): CA file path
                        • +
                        • CONFIG_MQTT_CLIENT_CLIENT_CERT_PATH (default: undefined): Client cert file path
                        • +
                        • CONFIG_MQTT_CLIENT_CLIENT_KEY_PATH (default: undefined): Client key file path
                        • +
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        MQTT
                        diff --git a/docs/modules/_mdf.js_openc2-core.Control.html b/docs/modules/_mdf.js_openc2-core.Control.html new file mode 100644 index 00000000..b80fe0b9 --- /dev/null +++ b/docs/modules/_mdf.js_openc2-core.Control.html @@ -0,0 +1 @@ +Control | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2-core._internal_.html b/docs/modules/_mdf.js_openc2-core._internal_.html new file mode 100644 index 00000000..512c7215 --- /dev/null +++ b/docs/modules/_mdf.js_openc2-core._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2-core.html b/docs/modules/_mdf.js_openc2-core.html new file mode 100644 index 00000000..efdfdf4c --- /dev/null +++ b/docs/modules/_mdf.js_openc2-core.html @@ -0,0 +1,29 @@ +@mdf.js/openc2-core | @mdf.js

                        Module @mdf.js/openc2-core

                        @mdf.js

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js

                        +
                        Typescript tools for development
                        + +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Control

                        Classes

                        Accessors
                        Consumer
                        ConsumerMap
                        Gateway
                        Producer
                        Registry

                        Interfaces

                        CommandJobHeader
                        ConsumerAdapter
                        ConsumerOptions
                        GatewayOptions
                        ProducerAdapter
                        ProducerOptions

                        Type Aliases

                        CommandJobDone
                        CommandJobHandler
                        OnCommandHandler
                        Resolver
                        ResolverEntry
                        ResolverMap
                        diff --git a/docs/modules/_mdf.js_openc2.Adapters.Dummy.html b/docs/modules/_mdf.js_openc2.Adapters.Dummy.html new file mode 100644 index 00000000..f0466836 --- /dev/null +++ b/docs/modules/_mdf.js_openc2.Adapters.Dummy.html @@ -0,0 +1,4 @@ +Dummy | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes

                        DummyConsumerAdapter
                        DummyProducerAdapter

                        Type Aliases

                        Config
                        diff --git a/docs/modules/_mdf.js_openc2.Adapters.Redis.html b/docs/modules/_mdf.js_openc2.Adapters.Redis.html new file mode 100644 index 00000000..acea646a --- /dev/null +++ b/docs/modules/_mdf.js_openc2.Adapters.Redis.html @@ -0,0 +1 @@ +Redis | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2.Adapters.SocketIO.html b/docs/modules/_mdf.js_openc2.Adapters.SocketIO.html new file mode 100644 index 00000000..409fc700 --- /dev/null +++ b/docs/modules/_mdf.js_openc2.Adapters.SocketIO.html @@ -0,0 +1 @@ +SocketIO | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2.Adapters.html b/docs/modules/_mdf.js_openc2.Adapters.html new file mode 100644 index 00000000..9f248312 --- /dev/null +++ b/docs/modules/_mdf.js_openc2.Adapters.html @@ -0,0 +1 @@ +Adapters | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2.Factory.html b/docs/modules/_mdf.js_openc2.Factory.html new file mode 100644 index 00000000..ea4732fc --- /dev/null +++ b/docs/modules/_mdf.js_openc2.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2._internal_.html b/docs/modules/_mdf.js_openc2._internal_.html new file mode 100644 index 00000000..ecff6c64 --- /dev/null +++ b/docs/modules/_mdf.js_openc2._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_openc2.html b/docs/modules/_mdf.js_openc2.html new file mode 100644 index 00000000..66d2d763 --- /dev/null +++ b/docs/modules/_mdf.js_openc2.html @@ -0,0 +1,29 @@ +@mdf.js/openc2 | @mdf.js

                        Module @mdf.js/openc2

                        @mdf.js

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js

                        +
                        Typescript tools for development
                        + +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Adapters
                        Factory

                        Classes

                        ServiceBus

                        Interfaces

                        AdapterOptions
                        ServiceBusOptions

                        Type Aliases

                        RedisClientOptions
                        SocketIOClientOptions
                        SocketIOServerOptions
                        diff --git a/docs/modules/_mdf.js_redis-provider.Redis.html b/docs/modules/_mdf.js_redis-provider.Redis.html new file mode 100644 index 00000000..a9053ed1 --- /dev/null +++ b/docs/modules/_mdf.js_redis-provider.Redis.html @@ -0,0 +1,4 @@ +Redis | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Type Aliases

                        Config
                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_redis-provider._internal_.html b/docs/modules/_mdf.js_redis-provider._internal_.html new file mode 100644 index 00000000..f06b4f0f --- /dev/null +++ b/docs/modules/_mdf.js_redis-provider._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_redis-provider.html b/docs/modules/_mdf.js_redis-provider.html new file mode 100644 index 00000000..b50a2a96 --- /dev/null +++ b/docs/modules/_mdf.js_redis-provider.html @@ -0,0 +1,69 @@ +@mdf.js/redis-provider | @mdf.js

                        Module @mdf.js/redis-provider

                        @mdf.js/redis-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/redis-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        Redis provider for @mdf.js based on ioredis.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/redis-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/redis-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Checks the ping messages from the server. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last ping event was successful, error if the provider is running and the last ping event was not successful.
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          +
                        • +
                        • memory: +
                            +
                          • observedValue: actual memory usage of the provider instance. The values are in expressed in bytes: used memory / max memory.
                          • +
                          • observedUnit: used memory / max memory.
                          • +
                          • status: fail if there is a problem getting the memory usage, or if the memory usage is greater or equal than 100% of the maximum memory, warn if the memory usage is greater than 80% of the maximum memory, pass in other case.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_REDIS_HOST (default: '127.0.0.1'): REDIS connection host
                        • +
                        • CONFIG_REDIS_PORT (default: 6379): REDIS connection port
                        • +
                        • CONFIG_REDIS_DB (default: 0): REDIS connection database
                        • +
                        • CONFIG_REDIS_PASSWORD (default: undefined): REDIS connection password
                        • +
                        • CONFIG_REDIS_RETRY_DELAY_FACTOR (default: 2000): REDIS connection retry delay factor
                        • +
                        • CONFIG_REDIS_RETRY_DELAY_MAX (default: 60000): REDIS connection retry delay max
                        • +
                        • CONFIG_REDIS_KEEPALIVE (default: 10000): REDIS connection keepAlive
                        • +
                        • CONFIG_REDIS_CONNECTION_TIMEOUT (default: 10000): REDIS connection keepAlive
                        • +
                        • CONFIG_REDIS_CHECK_INTERVAL (default: 60000): REDIS status check interval
                        • +
                        • CONFIG_REDIS_DISABLE_CHECKS (default: false): Disable Redis checks
                        • +
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Redis
                        diff --git a/docs/modules/_mdf.js_s3-provider.S3.html b/docs/modules/_mdf.js_s3-provider.S3.html new file mode 100644 index 00000000..ac9e8c2c --- /dev/null +++ b/docs/modules/_mdf.js_s3-provider.S3.html @@ -0,0 +1,4 @@ +S3 | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_s3-provider.html b/docs/modules/_mdf.js_s3-provider.html new file mode 100644 index 00000000..dcae17a6 --- /dev/null +++ b/docs/modules/_mdf.js_s3-provider.html @@ -0,0 +1,50 @@ +@mdf.js/s3-provider | @mdf.js

                        Module @mdf.js/s3-provider

                        @mdf.js/s3-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/s3-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        S3 provider for @mdf.js based on aws-sdk/client-s3.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/s3-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/s3-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +
                          +
                        • CONFIG_S3_REGION (default: 'eu-central-1'): S3 AWS region to which send requests
                        • +
                        • CONFIG_S3_ACCESS_KEY_ID (default: 'MY_ACCESS_KEY_ID'): S3 AWS connection access key identifier
                        • +
                        • CONFIG_S3_SECRET_ACCESS_KEY (default: 'MY_SECRET_ACCESS_KEY'): S3 AWS connection secret access key
                        • +
                        • NODE_APP_INSTANCE (default: 'MY_SECRET_ACCESS_KEY'): S3 AWS connection secret access key
                        • +
                        • CONFIG_S3_SERVICE_ID (default: process.env['NODE_APP_INSTANCE'] || CONFIG_ARTIFACT_ID): S3 unique service identifier
                        • +
                        • CONFIG_S3_PROXY_HTTP (default: undefined): HTTP Proxy URI
                        • +
                        • CONFIG_S3_PROXY_HTTPS (default: undefined): HTTPS Proxy URI
                        • +
                        • NODE_APP_INSTANCE: Default S3 unique service identifier
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        S3
                        diff --git a/docs/modules/_mdf.js_service-registry._internal_.html b/docs/modules/_mdf.js_service-registry._internal_.html new file mode 100644 index 00000000..81bdaf98 --- /dev/null +++ b/docs/modules/_mdf.js_service-registry._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_service-registry.html b/docs/modules/_mdf.js_service-registry.html new file mode 100644 index 00000000..2179e321 --- /dev/null +++ b/docs/modules/_mdf.js_service-registry.html @@ -0,0 +1,336 @@ +@mdf.js/service-registry | @mdf.js

                        Module @mdf.js/service-registry

                        @mdf.js/service-registry

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/service-registry

                        +
                        Service register, used for tooling microservices adding observability and control capabilities. +
                        + +
                        + +

                        The @mdf.js/service-register is a core package of the Mytra Development Framework. This module is designed for instrumenting microservices, adding observability and control capabilities, among other features. This allows developers to focus on the business logic development, instead of implementing these capabilities into each microservice.

                        +

                        In summary, the @mdf.js/service-register module provides the following features:

                        +
                          +
                        • Configuration management: Load configurations from files, environment variables, or the package.json file.
                        • +
                        • Logging: Use a logger with different transports, levels, and formats.
                        • +
                        • Metrics: Collect metrics from the application and expose them through an HTTP server in Prometheus format.
                        • +
                        • Health checks: Expose an HTTP server with health checks for the application.
                        • +
                        • Control Interface: Allow to create a built-in OpenC2 consumer for controlling the application.
                        • +
                        +

                        The @mdf.js/service-register module is intended to be loaded at the start of the application, even supporting the use of cluster for creating multiple instances of the application.

                        +

                        With default parameters:

                        +
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry();
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources +
                        + +

                        With custom parameters:

                        +
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry(
                        {
                        configFiles: ['./config/config.json'],
                        useEnvironment: true,
                        loadReadme: true,
                        },
                        {
                        loggerOptions: {
                        console: {
                        enabled: true,
                        level: 'info',
                        }
                        },
                        metadata: {
                        name: 'service-registry',
                        version: '1.0.0',
                        description: 'Service registry, used for tooling microservices with observability and control capabilities.',
                        }
                        ...
                        },
                        {
                        myOwnProperty: `myNeededValue`
                        }
                        );

                        const myProvider = new MyProvider(service.get('myProviderConfig'));
                        const myResource = new MyResource(service.get('myResourceConfig.option1'));
                        const myService = new MyService(service.settings.custom.myOwnProperty);
                        service.logger.info('My custom log message');
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources +
                        + +

                        Using cluster for creating multiple instances:

                        +
                        import { ServiceRegistry } from '@mdf.js/service-registry';
                        import cluster from 'cluster';

                        if (cluster.isMaster) {
                        const service = new ServiceRegistry(
                        {},
                        {
                        observabilityOptions: {
                        isCluster: true, // Necessary to indicate that the service is running in cluster mode
                        },
                        }
                        );
                        for (let i = 0; i < 4; i++) {
                        cluster.fork({
                        NODE_APP_INSTANCE: `MyOwnIdentifier-${i}`,
                        });
                        }
                        await service.start(); // Even with resources registered, they will not be started
                        } else {
                        const service = new ServiceRegistry();
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources
                        } +
                        + +
                        npm install @mdf.js/service-register
                        +
                        + +
                        yarn add @mdf.js/service-register
                        +
                        + +

                        To better understand how this module works, we will divide the documentation into several parts:

                        +
                          +
                        • Parameterization Options: Parameters that can be passed to the ServiceRegistry class constructor.
                        • +
                        • Module's Programmatic Interface: How to access the module's functionalities from the code.
                        • +
                        • Module's REST-API Interface: How to access the module's functionalities through a REST API.
                        • +
                        • Module's Control Interface: How to control the module's functionalities through a control interface.
                        • +
                        +
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry(
                        {
                        configFiles: ['./config/config.json'],
                        useEnvironment: true,
                        loadReadme: true,
                        },
                        {
                        loggerOptions: {
                        console: {
                        enabled: true,
                        level: 'info',
                        }
                        },
                        metadata: {
                        name: 'service-registry',
                        version: '1.0.0',
                        description: 'Service registry, used for tooling microservices with observability and control capabilities.',
                        }
                        ...
                        },
                        {
                        myOwnProperty: `myNeededValue`
                        }
                        ); +
                        + +
                          +
                        • BootstrapOptions: Service bootstrap options, primarily allowing configuration of how the module @mdf.js/service-registry loads its settings, enabling loading from files, environment variables, or even the project's package.json file.
                        • +
                        • ServiceRegistryOptions: Used as configuration values for the @mdf.js/service-registry module itself, such as the service name, version, description, etc. They override the default values or values loaded from other sources.
                        • +
                        • CustomOptions: Custom options, used as configuration values for the service being developed. These values can be accessed through the settings.custom property of the ServiceRegistry object. These properties override the default values or values loaded from other sources.
                        • +
                        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        PropertyTypeDescriptionDefault value
                        configFilesstring[]List of files with deploying options to be loaded. The entries could be a file path or glob pattern. It supports configurations in JSON, YAML, TOML, and .env file formats. Check @mdf.js/service-setup-provider for more details.[]
                        presetFilesstring[]List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset. Check @mdf.js/service-setup-provider for more details.[]
                        presetstringPreset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used. Check @mdf.js/service-setup-provider for more details.process.env['CONFIG_CUSTOM_PRESET'] process.env['CONFIG_SERVICE_REGISTRY_PRESET'] undefined
                        useEnvironmentbooleanFlag indicating that the environment configuration variables should be used. The configuration loaded by environment variables will be merged with the rest of the configuration, overriding the configuration from files, but not the configuration passed as argument to Service Registry. When option is set some filters are applied to the environment variables to avoid conflicts in the configuration.
                        The filters are:

                        - CONFIG_METADATA_: Application metadata configuration.
                        - CONFIG_OBSERVABILITY_: Observability service configuration.
                        - CONFIG_LOGGER_: Logger configuration.
                        - CONFIG_RETRY_OPTIONS_: Retry options configuration.
                        - CONFIG_ADAPTER_: Consumer adapter configuration.

                        The loader expect environment configuration variables represented in SCREAMING_SNAKE_CASE, that will parsed to camelCase and merged with the rest of the configuration. The consumer adapter configuration is an exception, due to the kind of configuration, it should be provided by configuration parameters.
                        false
                        loadReadmebooleanFlag indicating that the README.md file should be loaded. If this flag is set to true, the module will scale parent directories looking for a README.md file to load, if the file is found, the README content will be exposed in the observability endpoints. If the flag is a string, the string will be used as the file name to look for.false
                        loadPackagebooleanlag indicating that the package.json file should be loaded. If this flag is set to true, the the module will scale parent directories looking for a package.json file to load, if the file is found, the package information will be used to fullfil the metadata field.

                        - package.name will be used as the metadata.name.
                        - package.version will be used as the metadata.version, and the first part of the version will be used as the metadata.release.
                        - package.description will be used as the metadata.description.
                        - package.keywords will be used as the metadata.tags.
                        - package.config.${name}, where name is the name of the configuration, will be used to find the rest of properties with the same name that in the metadata.

                        This information will be merged with the rest of the configuration, overriding the configuration from files, but not the configuration passed as argument to Service Registry.
                        false
                        consumerbooleanFlag indicating if the OpenC2 Consumer command interface should be enabled. The command interface is a set of commands that can be used to interact with the application. The commands are exposed in the observability endpoints and can be used to interact with the service, or, if a consumer adapter is configured, to interact with the service from a central controller.false
                        +
                          +
                        • +

                          metadata (Metadata): Metadata information of the application or microservice. This information is used to identify the application in the logs, metrics, and traces... and is shown in the service observability endpoints.

                          +
                            +
                          • +

                            Properties:

                            +
                              +
                            • name (string): Name used to identify the application or microservice, it could be node name, or the name of the application.
                            • +
                            • description (string): Description of the application or microservice.
                            • +
                            • version (string): Version of the application or microservice.
                            • +
                            • release (string): Release of the application or microservice.
                            • +
                            • instanceId (string): Unique identifier of the application or microservice. This value is generated by the application if it is not provided.
                            • +
                            • serviceId (string): Human readable identifier of the application or microservice.
                            • +
                            • serviceGroupId (string): Group of the application or microservice to which it belongs.
                            • +
                            • namespace (string): Service namespace, used to identify declare which namespace the service belongs to. It must start with x- as it is a custom namespace and will be used for custom headers, openc2 commands, etc.
                            • +
                            • tags (string[]): Tags of the application or microservice.
                            • +
                            • links: service links to related services or resources. +
                                +
                              • self (string): Link to the service itself or the service observability endpoints.
                              • +
                              • related (string): Link to related services or resources of the service.
                              • +
                              • about (string): Link to the documentation of the service or the service README.
                              • +
                              +
                            • +
                            +
                          • +
                          • +

                            Default value:

                            +
                            {
                            name: 'mdf-app',
                            version: '0.0.0',
                            release: '0',
                            description: undefined,
                            instanceId: '12345678-1234-...', // This value is generated by the application
                            serviceId: 'mdf-service',
                            serviceGroupId: 'mdf-service-group',
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          consumerOptions (ConsumerOptions): OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 consumer, ff this configuration is not provided the consumer will not be started. The consumer is used to receive OpenC2 commands from a central controller.

                          +
                            +
                          • Properties: +
                              +
                            • id (string): Consumer identifier, used to identify the consumer in the system.
                            • +
                            • maxInactivityTime (number): Maximum time of inactivity before the consumer is stopped.
                            • +
                            • registerLimit (number): Maximum number of commands that can be registered at the same time.
                            • +
                            • retryOptions (RetryOptions): Retry options for the consumer.
                            • +
                            • actionTargetPairs (ActionTargetPairs): Action-Target pairs supported by the consumer. All the commands that are not in this list will be rejected, even if they has been included in the resolver map. If the command is only in this list, a command event will be emitted. Check below for more information.
                            • +
                            • profiles (string[]): Profiles supported by the consumer.
                            • +
                            • actuator (string[]): Actuator instance to be used by the consumer.
                            • +
                            • resolver (ResolverMap): Resolver map used by the consumer to resolve the commands. If a namespace is provided, a default resolver map will be included in order to provide a command interface for observability and control requests: +
                                +
                              • query:${namespace}:health: Query the health of the service.
                              • +
                              • query:${namespace}:stats: Query the metrics of the service.
                              • +
                              • query:${namespace}:errors: Query the errors of the service.
                              • +
                              • query:${namespace}:config: Query the configuration of the service.
                              • +
                              • start:${namespace}:resources: Start the resources of the service. (Only available if the service is NOT in cluster mode).
                              • +
                              • stop:${namespace}:resources: Stop the resources of the service. (Only available if the service is NOT in cluster mode).
                              • +
                              • restart:${namespace}:all: Kill the process, the service restart should be done by an external process manager.
                              • +
                              +
                            • +
                            +
                          • +
                          • Default value: undefined
                          • +
                          +
                        • +
                        • +

                          adapterOptions (AdapterOptions): Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, consumer and adapter options must be provided, in other case the consumer will start with a Dummy adapter with no connection to any external service, so only HTTP commands over the observability endpoints will be processed.

                          +
                            +
                          • Properties: +
                              +
                            • type (string): Type of the adapter, could be redis or socketio.
                            • +
                            • config (Redis.Config | SocketIO.Config): Configuration options for the adapter, depending on the type of adapter. Check the documentations of the providers @mdf.js/redis-provider and @mdf.js/socket-client-provider for more details.
                            • +
                            +
                          • +
                          • Default value: undefined
                          • +
                          +
                        • +
                        • +

                          observabilityOptions (ObservabilityOptions): Observability configuration options.

                          +
                            +
                          • +

                            Properties:

                            +
                              +
                            • port (number): Port of the observability server.
                            • +
                            • primaryPort (number): Primary port of the observability server in cluster mode, all the request to services over the port will be redirected to the primary port transparently.
                            • +
                            • host (string): Host of the observability server.
                            • +
                            • isCluster (boolean): Flag indicating that the service is running in cluster mode. If the service is running in cluster mode, the observability server will be started in all the instances of the cluster, but only the primary instance will be able to receive commands.
                            • +
                            • includeStack (boolean): Flag indicating that the stack trace should be included in the error register.
                            • +
                            • clusterUpdateInterval (number): Interval of time in milliseconds to update the cluster information.
                            • +
                            • maxSize (number): Maximum size of the error register.
                            • +
                            +
                          • +
                          • +

                            Default value:

                            +
                            {
                            primaryPort: 9080,
                            host: 'localhost',
                            isCluster: false,
                            includeStack: false,
                            clusterUpdateInterval: 10000,
                            maxSize: 100,
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          loggerOptions (LoggerOptions): Logger Options. If provided, a logger instance from the @mdf.js/logger package will be created and used by the application in all the internal services of the Application Wrapper. At the same time, the logger is exposed to the application to be used in the application services. If this options is not provided, a Debug logger will be used internally, but it will not be exposed to the application.

                          +
                            +
                          • +

                            Properties: check the documentation of the package @mdf.js/logger.

                            +
                          • +
                          • +

                            Default value:

                            +
                            {
                            console: {
                            enabled: true,
                            level: 'info',
                            },
                            file: {
                            enabled: false,
                            level: 'info',
                            },
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          retryOptions (RetryOptions): Retry options. If provided, the application will use this options to retry to start the services/resources registered in the Application Wrapped instance. If this options is not provided, the application will not retry to start the services/resources.

                          +
                            +
                          • +

                            Properties: check the documentation of the package @mdf.js/utils.

                            +
                          • +
                          • +

                            Default value:

                            +
                            {
                            attempts: 3,
                            maxWaitTime: 10000,
                            timeout: 5000,
                            waitTime: 1000,
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          configLoaderOptions (ConfigLoaderOptions): Configuration loader options. These options is used to load the configuration information of the application that is been wrapped by the Application Wrapper. This configuration could be loaded from files or environment variables, or even both.

                          +

                          To understand the configuration loader options, check the documentation of the package @mdf.js/service-setup-provider.

                          +
                          +

                          Note: Use different files for Application Wrapper configuration and for your own services to avoid conflicts.

                          +
                          +
                            +
                          • +

                            Properties: check the documentation of the package @mdf.js/service-setup-provider.

                            +
                          • +
                          • +

                            Default value:

                            +
                            {
                            configFiles: ['./config/custom/*.*'],
                            presetFiles: ['./config/custom/presets/*.*'],
                            schemaFiles: ['./config/custom/schemas/*.*'],
                            preset: process.env['CONFIG_CUSTOM_PRESET'] || process.env['CONFIG_SERVICE_REGISTRY_PRESET'],
                            useEnvironment: false,
                            loadReadme: false,
                            loadPackage: false,
                            } +
                            + +
                          • +
                          +
                        • +
                        +

                        These options are used to provide custom configuration to the services that are been wrapped by the Service Registry. These options are accessible through the settings.custom or customSettings property of the ServiceRegistry object. The options that you provide here will be merged with the rest of the configuration loaded based on the configLoaderOptions, being the last one the one that will override the rest of the configuration, in this way, you can create your own way to select the configuration that you want to use in your services, besides the use of the integrated @mdf.js/service-setup-provider for this purpose.

                        +
                          +
                        • Properties: +
                            +
                          • errors (ErrorRecord[]): Errors recorded by the application, the maximum size of the error register is defined by the maxSize option in the observabilityOptions.
                          • +
                          • health (Layer.App.Health): Health object, check the documentation of the package @mdf.js/core for more details.
                          • +
                          • status (Health.Status): Service status, check the documentation of the package @mdf.js/core for more details.
                          • +
                          • serviceRegistrySettings (ServiceRegistrySettings): final configuration parameters which are used by the service registry.
                          • +
                          • customSettings (CustomSettings): final result of the custom parameters.
                          • +
                          • settings (ServiceSetting): final result of the settings.
                          • +
                          +
                        • +
                        • Methods: +
                            +
                          • register(resource: Layer.Observable | Layer.Observable[]): void: Register a resource or an array of resources to the observability services of the application. If the resource fullfil the Layer.App.Resource or Layer.App.Service interfaces, the resource will be started when the application starts. Check the documentation of the package @mdf.js/core for more details.
                          • +
                          • get<T>(path: string | string[], defaultValue: T): T | undefined: Get a configuration value by path from the settings. If the path is not found, the default value will be returned.
                          • +
                          • get<P extends keyof CustomSettings>(key: P, defaultValue: CustomSettings[P]): CustomSettings[P] | undefined: Get a custom configuration value by key from the custom settings. If the key is not found, the default value will be returned.
                          • +
                          • async start(): Promise<void>: Start the application, this method will start all the resources registered in the application. If the application is running in cluster mode, only the primary instance will start the resources.
                          • +
                          • async stop(): Promise<void>: Stop the application, this method will stop all the resources registered in the application. If the application is running in cluster mode, only the primary instance will stop the resources.
                          • +
                          +
                        • +
                        • Events: +
                            +
                          • on(event: 'command', listener: (job: CommandJobHandler) => void): this: Event emitted when a command is received by the consumer. The event listener will receive a CommandJobHandler object with the command information. See below for more information.
                          • +
                          +
                        • +
                        +

                        By default the observability server is started in the port 9080, over the localhost. The observability server exposes the following endpoints:

                        +
                          +
                        • http://${host}:${port}/v${release}/health: Health check endpoint, returns the health of the service.
                        • +
                        • http://${host}:${port}/v${release}/metrics?json=true: Metrics endpoint, returns the metrics of the service in Prometheus format, if the query parameter json=true is provided, the metrics will be returned in JSON format.
                        • +
                        • http://${host}:${port}/v${release}/registry: Errors endpoint, returns the errors registered by the service, the maximum size of the error register is defined by the maxSize option in the observabilityOptions.
                        • +
                        +

                        If a consumer adapter is configured, the observability server will expose the following endpoints:

                        +
                          +
                        • http://${host}:${port}/v${release}/openc2/command: OpenC2 command interface, used to send OpenC2 commands to the service. See below for more information.
                        • +
                        • http://${host}:${port}/v${release}/openc2/jobs: OpenC2 jobs interface, used to query the jobs registered by the service.
                        • +
                        • http://${host}:${port}/v${release}/openc2/pendingJobs: OpenC2 pending jobs interface, used to query the pending jobs registered by the service.
                        • +
                        • http://${host}:${port}/v${release}/openc2/messages: OpenC2 messages interface, used to query the messages registered by the service.
                        • +
                        +

                        If the user register a service that fullfil the Layer.App.Service interface, including the Links and Router properties, the service will be started when the application starts, and the service will be exposed in the observability endpoints. Check the documentation of the package @mdf.js/core for more details.

                        +

                        The @mdf.js/service-registry module use the OpenC2 as Command and Control Interface (CCI).

                        +

                        This interface are based on the two modules of @mdf.js:

                        +
                          +
                        • @mdf.js/openc2-core: module that implement the OpenC2 core specification for Consumer, Provider and Gateway entities, not attached to any transport layer.
                        • +
                        • @mdf.js/openc2: module that implement a tooling interface, to allow the use of OpenC2 entities over several transport layers: MQTT, Redis Pub/Sub, AMQP, SocketIO ...
                        • +
                        +

                        Please check the documentation of the packages @mdf.js/openc2 and @mdf.js/openc2-core, and the OpenC2 specification for more details.

                        +
                          +
                        • CONFIG_CUSTOM_PRESET (default: undefined): Custom config preset selector, used to load a specific preset from the custom config folder. Default files to search for are ./config/custom/presets/\*.preset.\* This preset is used for the custom config.
                        • +
                        • CONFIG_SERVICE_REGISTRY_PRESET (default: undefined): Service registry preset selector, used to load a specific preset from the service registry config folder. Default files to search for are ./config/presets/\*.preset.\* This preset is used for the service registry config.
                        • +
                        • CONFIG_APP_NAME (default: 'mdf-app'): Application name
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Classes

                        ServiceRegistry

                        Interfaces

                        BootstrapOptions
                        ExtendedCrashObject
                        ExtendedMultiObject
                        ObservabilityServiceOptions
                        ServiceRegistryOptions
                        ServiceRegistrySettings
                        ServiceSetting

                        Type Aliases

                        ConsumerAdapterOptions
                        CustomSetting
                        CustomSettings
                        ErrorRecord
                        diff --git a/docs/modules/_mdf.js_service-setup-provider.Setup.html b/docs/modules/_mdf.js_service-setup-provider.Setup.html new file mode 100644 index 00000000..542880f7 --- /dev/null +++ b/docs/modules/_mdf.js_service-setup-provider.Setup.html @@ -0,0 +1 @@ +Setup | @mdf.js

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory

                        References

                        Client → ConfigManager
                        diff --git a/docs/modules/_mdf.js_service-setup-provider._internal_.html b/docs/modules/_mdf.js_service-setup-provider._internal_.html new file mode 100644 index 00000000..73e6ab7a --- /dev/null +++ b/docs/modules/_mdf.js_service-setup-provider._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_service-setup-provider.html b/docs/modules/_mdf.js_service-setup-provider.html new file mode 100644 index 00000000..894e6226 --- /dev/null +++ b/docs/modules/_mdf.js_service-setup-provider.html @@ -0,0 +1,139 @@ +@mdf.js/service-setup-provider | @mdf.js

                        Module @mdf.js/service-setup-provider

                        @mdf.js/service-setup-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/service-setup-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        @mdf.js/service-setup-provider is a versatile tool designed for handling, validating, and managing different sources of configuration information in Node.js applications. It provides robust support for environment-specific configurations, presets, and schema validation, making it an essential utility for projects that require dynamic configuration management. It supports configurations in JSON, YAML, TOML, and .env file formats and environment variables, allowing developers to define and manage configurations in a structured and consistent manner.

                        +

                        This module is designed and developed to facilitate the deployment of applications that operate in container environments where the same application is deployed in different contexts, such as development, testing, production, installation type A, installation type B, etc. This is the case for applications that are deployed in container environments, like Kubernetes, Docker, etc., especially in Edge Computing environments, where the application is deployed in different geographical locations, with different network configurations, hardware, etc.

                        +

                        In each context, the application needs to be configured differently, with configuration errors being common, especially in applications where fine-tuning of operation is achieved through a large number of configuration variables.

                        +

                        In these environments, it would be ideal to have a series of predefined configurations that fit each context, so that in the application deployment process, one only needs to choose the context (the predefined configuration) they wish to use, without the need for manual adjustments in the configuration variables.

                        +

                        At the same time, it must be possible, especially for the configuration of secrets, to have the ability to adjust environment variables that are loaded into the container at the time of execution, so that the configuration of secrets is not found in the application's source code or in configuration files, but the final result is the union of both configurations.

                        +
                          +
                        • Load and merge configuration files from various formats (JSON, YAML, TOML, .env), allowing a hierarchical configuration structure, merging configurations from different sources.
                        • +
                        • Load environment variables and merge with the rest of the configuration sources, handling environment-specific configurations with ease.
                        • +
                        • Validate configurations against a defined schema, using JSON Schema files.
                        • +
                        • Built-in express.js router for exposing configuration details over HTTP.
                        • +
                        +

                        Using npm:

                        +
                        npm install @mdf.js/service-setup-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/service-setup-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        This module is developed as a @mdf.js Provider so that it can be used easily in any application, both in the @mdf.js environment and in any other Node.js application.

                        +

                        In order to use this module, your should use the Factory exposed and create an instance using the create method:

                        +
                        import { Factory } from '@mdf.js/service-setup-provider';

                        const default = Factory.create(); // Create a new instance with default options

                        const custom = Factory.create({
                        config: {...} // Custom options
                        name: 'custom' // Custom name
                        useEnvironment: true // Use environment variables
                        logger: myLoggerInstance // Custom logger
                        }); +
                        + +

                        The configuration options (config) are the following:

                        +
                          +
                        • +

                          configFiles: List of configuration files to be loaded. The entries could be a file path or glob pattern. All the files will be loaded and merged in the order they are founded. The result of the merge will be used as the final configuration.

                          +

                          Some examples:

                          +
                          ['./config/*.json']
                          ['./config/*.json', './config/*.yaml']
                          ['./config/*.json', './config/*.yaml', './config/*.yml'] +
                          + +
                        • +
                        • +

                          presetFiles: List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset.

                          +

                          Some examples:

                          +
                          ['./config/presets/*.json']
                          ['./config/presets/*.json', './config/presets/*.yaml']
                          ['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml'] +
                          + +
                        • +
                        • +

                          envPrefix: Prefix or prefixes to use on configuration loading from the environment variables. The prefix will be used to filter the environment variables. The prefix will be removed from the environment variable name and the remaining part will be used as the configuration property name. The configuration property name will be converted to camel case. Environment variables will override the configuration from the configuration files.

                          +

                          Some examples:

                          +
                          `MY_APP_` // as single prefix
                          ['MY_APP_', 'MY_OTHER_APP_'] // as array of prefixes
                          { MY_APP: 'myApp', MY_OTHER_APP: 'myOtherApp' } // as object with prefixes +
                          + +
                        • +
                        +
                        +

                        Note: is important not misunderstand the envPrefix option and useEnvironment option of the Factory.

                        +
                          +
                        • envPrefix: this option is used to filter the environment variables that will be used to override the configuration from the configuration files. The envPrefix will affect only to the result of the final configuration object that this module is going to create.
                        • +
                        • useEnvironment: this option of the create method will be used to indicate if the environment variables will be used, or not, to override the configuration of this module.
                        • +
                        +
                        +
                          +
                        • schemaFiles: List of files with JSON schemas used to validate the configuration. The entries could be a file path or glob pattern.
                        • +
                        +

                        In this point we have:

                        +
                          +
                        • A config object as result of the merge of the configuration files.
                        • +
                        • A collection of presets objects as result of the merge of the preset files.
                        • +
                        • A environment object as result of parse the environment variables based on the envPrefix option.
                        • +
                        • A collection of schemas objects as result of the schema files.
                        • +
                        +

                        What we have to configure now is if we want to use a preset file and which one and if we want to validate the result based in a JSON schema. For this we have the following options:

                        +
                          +
                        • preset: Preset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used.
                        • +
                        • schema: Schema to be used to validate the configuration. If none is indicated, the configuration will not be validated. The schema name should be the same as the file name without the extension.
                        • +
                        • checker: DoorKeeper instance to be used to validate the configuration. If none is indicated, the setup instance will be try to create a new DoorKeeper instance using the schema files indicated in the options. If the schema files are not indicated, the configuration will not be validated.
                        • +
                        • base: Object to be used as base and main configuration options. The configuration will be merged with the configuration from the configuration files. This object will override the configuration from the configuration files and the environment variables. The main reason of this option is to allow the user to define some configuration in the code and let the rest of the configuration to be loaded, using the Configuration Manager as unique source of configuration.
                        • +
                        • default: Object to be used as default configuration options. The configuration will be merged with the configuration from the configuration files, the environment variables and the base option. This object will be used as the default configuration if no other configuration is found.
                        • +
                        +

                        The preset option is used to indicate which preset will be used as the base configuration. The preset name should be the same as the file name without the extension. The preset will be merged with the configuration from the configuration files and the environment variables. The preset will override the configuration from the configuration files and the environment variables will override the preset.

                        +

                        Once the instance is created, you can access to the ConfigManager instance using the client property of the Provider. The ConfigManager instance has the following methods and properties:

                        +
                          +
                        • Properties: +
                            +
                          • defaultConfig: configuration object with the result of the merge of the configuration files.
                          • +
                          • envConfig: configuration object with the result of the merge the environment variables.
                          • +
                          • presets: Collection of presets objects with the result of the merge of the preset files.
                          • +
                          • preset: selected present.
                          • +
                          • schema: selected schema.
                          • +
                          • nonDisclosureConfig: configuration object with the result of the merge of the configuration files, the preset WITHOUT the environment variables. In environments variables is where we should store the secrets.
                          • +
                          • config: Configuration object with the result of the merge of the configuration files, the preset and the environment variables.
                          • +
                          • isErrored: boolean that indicates if the configuration is valid or not.
                          • +
                          • error: a Multi instance with the errors found in the configuration validation if the configuration is not valid.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_SERVICE_SETUP_PRESET_FILES (default: './config/presets/*.*'): List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset.
                        • +
                        • CONFIG_SERVICE_SETUP_SCHEMA_FILES (default: './config/schemas/*.*'): List of files with JSON schemas used to validate the configuration. The entries could be a file path or glob pattern.
                        • +
                        • CONFIG_SERVICE_SETUP_CONFIG_FILES (default: './config/*.*'): List of configuration files to be loaded. The entries could be a file path or glob pattern. All the files will be loaded and merged in the order they are founded. The result of the merge will be used as the final configuration.
                        • +
                        • CONFIG_SERVICE_SETUP_PRESET (default: undefined): Preset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used.
                        • +
                        • CONFIG_SERVICE_SETUP_SCHEMA (default: undefined): Schema to be used to validate the configuration. If none is indicated, the configuration will not be validated. The schema name should be the same as the file name without the extension.
                        • +
                        • CONFIG_SERVICE_SETUP_ENV_PREFIX (default: undefined): Prefix or prefixes to use on configuration loading from the environment variables. The prefix will be used to filter the environment variables. The prefix will be removed from the environment variable name and the remaining part will be used as the configuration property name. The configuration property name will be converted to camel case. Environment variables will override the configuration from the configuration files.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Modules

                        <internal>

                        Namespaces

                        Setup

                        Classes

                        ConfigManager
                        diff --git a/docs/modules/_mdf.js_socket-client-provider.SocketIOClient.html b/docs/modules/_mdf.js_socket-client-provider.SocketIOClient.html new file mode 100644 index 00000000..36663841 --- /dev/null +++ b/docs/modules/_mdf.js_socket-client-provider.SocketIOClient.html @@ -0,0 +1,4 @@ +SocketIOClient | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        Config

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_socket-client-provider.html b/docs/modules/_mdf.js_socket-client-provider.html new file mode 100644 index 00000000..614d6bff --- /dev/null +++ b/docs/modules/_mdf.js_socket-client-provider.html @@ -0,0 +1,57 @@ +@mdf.js/socket-client-provider | @mdf.js

                        Module @mdf.js/socket-client-provider

                        @mdf.js/socket-client-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/socket-client-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        Socket client provider for @mdf.js based on socket.io-client.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/socket-client-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/socket-client-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Check the status of the connection to the socket-io server based on the connection and disconnection events from the socket-io client. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and connected to the server, error if the provider is running but disconnected from the server.
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_SOCKET_IO_CLIENT_URL (default: 'http://localhost:8080'): URL of the server
                        • +
                        • CONFIG_SOCKET_IO_CLIENT_PATH (default: '/socket.io'): Path where the server will listen
                        • +
                        • CONFIG_SOCKET_IO_CLIENT_TRANSPORTS (default: ['websocket']): Transports to use
                        • +
                        • CONFIG_SOCKET_IO_CLIENT_CA_PATH (default: undefined): CA file path
                        • +
                        • CONFIG_SOCKET_IO_CLIENT_CLIENT_CERT_PATH (default: undefined): Client cert file path
                        • +
                        • CONFIG_SOCKET_IO_CLIENT_CLIENT_KEY_PATH (default: undefined): Client key file path
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        SocketIOClient
                        diff --git a/docs/modules/_mdf.js_socket-server-provider.SocketIOServer.html b/docs/modules/_mdf.js_socket-server-provider.SocketIOServer.html new file mode 100644 index 00000000..a4d11e77 --- /dev/null +++ b/docs/modules/_mdf.js_socket-server-provider.SocketIOServer.html @@ -0,0 +1,4 @@ +SocketIOServer | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +

                        Classes - Provider

                        Port

                        Interfaces

                        BasicAuthentication
                        Config
                        ConnectionError
                        InstrumentOptions

                        Type Aliases

                        Provider

                        Variables

                        Factory
                        diff --git a/docs/modules/_mdf.js_socket-server-provider.html b/docs/modules/_mdf.js_socket-server-provider.html new file mode 100644 index 00000000..377d4575 --- /dev/null +++ b/docs/modules/_mdf.js_socket-server-provider.html @@ -0,0 +1,57 @@ +@mdf.js/socket-server-provider | @mdf.js

                        Module @mdf.js/socket-server-provider

                        @mdf.js/socket-server-provider

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/socket-server-provider

                        +
                        Typescript tools for development
                        + +
                        + +

                        Socket server provider for @mdf.js based on socket.io.

                        +

                        Using npm:

                        +
                        npm install @mdf.js/socket-server-provider
                        +
                        + +

                        Using yarn:

                        +
                        yarn add @mdf.js/socket-server-provider
                        +
                        + +

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        +

                        Checks included in the provider:

                        +
                          +
                        • status: Check the status of the server based in the status of the listening port and the error events from the socket-io server. +
                            +
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the server is listening and error if the provider is running but the server is not listening.
                          • +
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • +
                          +
                        • +
                        +
                          +
                        • CONFIG_SOCKET_IO_SERVER_PORT (default: 8080): Port where the server will listen
                        • +
                        • CONFIG_SOCKET_IO_SERVER_HOST (default: 'localhost'): Host where the server will listen
                        • +
                        • CONFIG_SOCKET_IO_SERVER_PATH (default: '/socket.io'): Path where the server will listen
                        • +
                        • CONFIG_SOCKET_IO_SERVER_ENABLE_UI (default: true): Enable the UI
                        • +
                        • CONFIG_SOCKET_IO_SERVER_CORS__ORIGIN (default: `/[\s\S]*\/`): CORS origin
                        • +
                        • CONFIG_SOCKET_IO_SERVER_CORS__CREDENTIALS (default: true): CORS credentials
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        Namespaces

                        SocketIOServer
                        diff --git a/docs/modules/_mdf.js_tasks._internal_.html b/docs/modules/_mdf.js_tasks._internal_.html new file mode 100644 index 00000000..e469e022 --- /dev/null +++ b/docs/modules/_mdf.js_tasks._internal_.html @@ -0,0 +1 @@ +<internal> | @mdf.js
                        diff --git a/docs/modules/_mdf.js_tasks.html b/docs/modules/_mdf.js_tasks.html new file mode 100644 index 00000000..1505efe2 --- /dev/null +++ b/docs/modules/_mdf.js_tasks.html @@ -0,0 +1,315 @@ +@mdf.js/tasks | @mdf.js

                        Module @mdf.js/tasks

                        @mdf.js/tasks

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js

                        +
                        Typescript tools for development
                        + +
                        + +

                        The @mdf.js/tasks package is a set of tools designed to facilite the development of services that require the execution of tasks in a controlled manner. The package is composed of the following elements:

                        +
                          +
                        • Tasks: Single, Group or Sequence are the types of tasks that can be executed, each one extends the TaskHandler class, that provides the basic functionality to manage the task, and include some additional properties and methods to control the execution of specific kind of tasks. +
                            +
                          • Single: A single task that can be executed.
                          • +
                          • Group: A group of tasks that can be executed in parallel.
                          • +
                          • Sequence: A specific sequence needed to execute a concrete task, allowing to define pre, post and finally tasks, besides the main task.
                          • +
                          +
                        • +
                        • Limiter: A class that allows to control the number of tasks that can be executed in parallel.
                        • +
                        • Scheduler: A class that allows to schedule the execution of tasks in a specific time.
                        • +
                        +

                        Each element is designed to be used together with the others, but tasks can be used independently if needed.

                        +

                        To install the @mdf.js/tasks package, you can use the following commands:

                        +
                          +
                        • NPM:
                        • +
                        +
                        npm install @mdf.js/tasks
                        +
                        + +
                          +
                        • Yarn:
                        • +
                        +
                        yarn add @mdf.js/tasks
                        +
                        + +

                        Tasks are the main element of the package, based in the TaskHandler class, that provides the basic functionality to manage the task, and include some additional properties and methods to control the execution of specific kind of tasks. These tasks acts as instances of task execution requests, allowing to control the execution and result. The Single task is the basic task, Groupand Sequence are different ways to execute Single tasks, allowing to resolve more complex scenarios.

                        +

                        As each task type extends the TaskHandler class, let's see the basic properties and methods that are common to all of them:

                        +
                          +
                        • +

                          Properties:

                          +
                            +
                          • +

                            uuid (string): The unique identifier of the task instance.

                            +
                          • +
                          • +

                            taskId (string): The identifier of the task, defined by the user.

                            +
                          • +
                          • +

                            createdAt (Date): The date and time when the task was created.

                            +
                          • +
                          • +

                            priority (number): The priority of the task, used to order the execution of tasks in Limiter or Scheduler.

                            +
                          • +
                          • +

                            weight (number): The weight of the task, used in the Limiter to control the number of tasks that can be executed in parallel.

                            +
                          • +
                          • +

                            metadata (Metadata): The task metadata object that contains all the relevant information about the task and its execution.

                            +
                            /** Metadata of the execution of the task */
                            export interface MetaData {
                            /** Unique task identification, unique for each task */
                            uuid: string;
                            /** Task identifier, defined by the user */
                            taskId: string;
                            /** Status of the task */
                            status: TaskState;
                            /** Date when the task was created */
                            createdAt: string;
                            /** Date when the task was executed in ISO format */
                            executedAt?: string;
                            /** Date when the task was completed in ISO format */
                            completedAt?: string;
                            /** Date when the task was cancelled in ISO format */
                            cancelledAt?: string;
                            /** Date when the task was failed in ISO format */
                            failedAt?: string;
                            /** Reason of failure or cancellation */
                            reason?: string;
                            /** Duration of the task in milliseconds */
                            duration?: number;
                            /** Task priority */
                            priority: number;
                            /** Task weight */
                            weight: number;
                            /** Additional metadata objects, store the metadata information from related tasks in a sequence or group */
                            $meta?: MetaData[];
                            } +
                            + +
                          • +
                          +
                        • +
                        • +

                          Methods:

                          +
                            +
                          • async execute(): Promise<Result>: Executes the task, returning a promise with the result of the execution.
                          • +
                          • async cancel(error?: Crash): void: Cancels the task execution.
                          • +
                          +
                        • +
                        +

                        All the different tasks constructors, besides other parameters, allow to configure the task execution with the following options (TaskOptions):

                        +
                          +
                        • id (string): The identifier of the task, defined by the user, if not provided, a random identifier will be generated.
                        • +
                        • priority (number): The priority of the task, used to order the execution of tasks in Limiter or Scheduler. Default is 0.
                        • +
                        • weight (number): The weight of the task, used in the Limiter to control the number of tasks that can be executed in parallel. Default is 1.
                        • +
                        • retryOptions (RetryOptions): The options to retry the task in case of failure. Check the RetryOptions interface for more information in the @mdf.js/utils package.
                        • +
                        • bind (any): The object to bind the task to, if the task is a method of a class.
                        • +
                        • retryStrategy (RetryStrategy): The strategy to retry the task in case of execute method being called again. Possible values are: +
                            +
                          • retry (RETRY_STRATEGY.RETRY): The task will allow to retry the execution again if it fails, updating the metadata in each retry.
                          • +
                          • failAfterSuccess (RETRY_STRATEGY.FAIL_AFTER_SUCCESS): The task will allow to be executed again if it fails, but it will rejects if there are more retries before the success.
                          • +
                          • failAfterExecuted (RETRY_STRATEGY.FAIL_AFTER_EXECUTED): The task will allow only one execution, if it fails, it will fail in every retry.
                          • +
                          • notExecAfterSuccess (RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS): The task will resolve the result of first successful execution, if it fails, it will allow to be executed again.
                          • +
                          +
                        • +
                        +

                        The Single task is the basic task, it has not more options than the TaskHandler class, but it can be used to execute any kind of task, as a function or a method of a class. The Single task can be used to execute a single task, and it can be used in combination with the Limiter or Scheduler classes to control the execution of tasks.

                        +
                        import { Single, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        // Any kind of promise can be used as task
                        function task(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        // Or a method of a class
                        class MyClass {
                        task(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        }

                        const myInstance = new MyClass();

                        // A task can be created with a function
                        const unBindedTask = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        unBindedTask.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        });

                        // Or binded to a class instance
                        const bindedTask = new Single(myInstance.task, 5, {
                        id: 'task2',
                        bind: myInstance,
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        bindedTask.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        }); +
                        + +

                        The Group task is a set of tasks that can be executed in order. The Result of the execution of the group is an array with the results of each task, and the $meta property of the metadata object contains the metadata of each task.

                        +

                        The constructor of the Group has the next parameters:

                        +
                          +
                        • tasks (TaskHandler[]): The tasks to be executed in the group.
                        • +
                        • options (TaskOptions): The options to configure the group task execution.
                        • +
                        • atLeastOne (boolean): If true, the group will resolve the result if at least one task is resolved, if false, all the tasks must be resolved to resolve the group.
                        • +
                        +
                        import { Group, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        const tasks = [
                        new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ];

                        const group = new Group(tasks, {
                        id: 'group1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        group.on('done', (uuid: string, result: number[], meta: Metadata, error?: Crash) => {
                        console.log('Group done', uuid, result, meta, error);
                        }); +
                        + +

                        The Sequence task is a special king of task that need to execute a sequence of tasks in a specific order. The Sequence task allows to define pre, post and finally tasks, besides the main task. The Result of the execution of the sequence is the result of the main task, and the $meta property of the metadata object contains the metadata of each task.

                        +

                        The constructor of the Sequence has the next parameters:

                        +
                          +
                        • pattern (SequencePattern): The pattern of the sequence, that defines the pre, post, main and finally tasks: +
                            +
                          • pre (TaskHandler[]): The tasks to be executed before the main task.
                          • +
                          • task (TaskHandler): The main task to be executed.
                          • +
                          • post (TaskHandler[]): The tasks to be executed after the main task, if the main task fails, the post tasks will not be executed.
                          • +
                          • finally (TaskHandler[]): The tasks to be executed at the end of the sequence, even if the main task fails.
                          • +
                          +
                        • +
                        • options (TaskOptions): The options to configure the sequence task execution.
                        • +
                        +
                        import { Sequence, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        const sequence = new Sequence(
                        {
                        pre: [
                        new Single(task, 5, {
                        id: 'pre1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        task: new Single(task, 10, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        post: [
                        new Single(task, 15, {
                        id: 'post1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        finally: [
                        new Single(task, 20, {
                        id: 'finally1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        },
                        {
                        id: 'sequence1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }
                        );

                        sequence.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Sequence done', uuid, result, meta, error);
                        }); +
                        + +

                        The Limiter class allows to control the execution of tasks, limiting the number of tasks that can be executed in parallel, the order of the execution based in the priority of the tasks, the cadence of the execution and "throughput", controlling the number of tasks that can be executed in a specific time.

                        +

                        The Limiter accepts tasks of any kind, Single, Group or Sequence, allowing to schedule the execution of the tasks or execute them, taking always into account the Limiter configuration.

                        +

                        In order to create a new Limiter instance, the constructor accepts a LimiterOptions object with the following properties:

                        +
                          +
                        • concurrency (number): The maximum number of concurrent jobs. Default is 1.
                        • +
                        • delay (number): Delay between each job in milliseconds. Default is 0. For concurrency = 1, the delay is applied after each job is finished. For concurrency > 1, if the actual number of concurrent jobs is less than concurrency, the delay is applied after each job is finished, otherwise, the delay is applied after each job is started.
                        • +
                        • retryOptions (RetryOptions): Set the default options for the retry process of the jobs. Default is undefined. Check the RetryOptions interface for more information in the @mdf.js/utils package.
                        • +
                        • autoStart (boolean): Set whether the limiter should start to process the jobs automatically. Default is true.
                        • +
                        • highWater (number): The maximum number of jobs in the queue. Default is Infinity.
                        • +
                        • strategy (Strategy): The strategy to use when the queue length reaches highWater. Default is 'leak'. Possible values are: +
                            +
                          • leak (STRATEGY.LEAK): When adding a new job to a limiter, if the queue length reaches highWater, drop the oldest job with the lowest priority. This is useful when jobs that have been waiting for too long are not important anymore. If all the queued jobs are more important (based on their priority value) than the one being added, it will not be added.
                          • +
                          • overflow (STRATEGY.OVERFLOW): When adding a new job to a limiter, if the queue length reaches highWater, do not add the new job. This strategy totally ignores priority levels.
                          • +
                          • overflow-priority (STRATEGY.OVERFLOW_PRIORITY): Same as LEAK, except it will only drop jobs that are less important than the one being added. If all the queued jobs are as or more important than the new one, it will not be added.
                          • +
                          • block (STRATEGY.BLOCK): When adding a new job to a limiter, if the queue length reaches highWater, the limiter falls into "blocked mode". All queued jobs are dropped and no new jobs will be accepted until the limiter unblocks. It will unblock after penalty milliseconds have passed without receiving a new job. penalty is equal to 15 * minTime (or 5000 if minTime is 0) by default. This strategy is ideal when bruteforce attacks are to be expected. This strategy totally ignores priority levels.
                          • +
                          +
                        • +
                        • penalty (number): The penalty for the BLOCK strategy in milliseconds. Default is 0.
                        • +
                        • bucketSize (number): The bucket size for the rate limiter. Default is 0. If the bucket size is 0, only concurrency and delay will be used to limit the rate of the jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to limit the rate of the jobs. The bucket size is the maximum number of tokens that can be consumed in the interval. The interval is defined by the tokensPerInterval and interval properties.
                        • +
                        • tokensPerInterval (number): Define the number of tokens that will be added to the bucket at the beginning of the interval. Default is 1.
                        • +
                        • interval (number): Define the interval in milliseconds. Default is 1000.
                        • +
                        +
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        }); +
                        + +

                        The Limiter class allows to:

                        +
                          +
                        • schedule the execution of tasks, that means that the tasks are added to the queue, and they will be executed when the limiter is ready to process them. When the task is executed two events: done and an event with the taskId, both of them with the same information: +
                            +
                          • on('done' | taskId, listener: (uuid: string, result: Result, meta: MetaData, error?: Crash) => void): this:
                          • +
                          • uuid: The unique identifier of the task instance.
                          • +
                          • result: The result of the task execution.
                          • +
                          • meta: The metadata of the task execution.
                          • +
                          • error: The error in case of failure.
                          • +
                          +
                        • +
                        • execute the task, that will wait until the limiter is ready to process the task, and execute it, resolving the result of the task execution.
                        • +
                        +

                        There are several methods to interact with the limiter and control the execution of the tasks:

                        +
                          +
                        • start(): void: Start the limiter, allowing to process the tasks in the queue. If the limiter is already started, it will not do anything. If autoStart is true, the limiter will start automatically when a task is added to the queue.
                        • +
                        • stop(): void: Stop the limiter, preventing to process the tasks in the queue. If the limiter is already stopped, it will not do anything.
                        • +
                        • waitUntilEmpty(): Promise<void>: Wait until the queue is empty.
                        • +
                        • clear(): void: Clear the queue, removing all the tasks in the queue.
                        • +
                        +

                        And several properties to get information about the limiter:

                        +
                          +
                        • size (number): The number of tasks in the queue.
                        • +
                        • pending (number): The number of tasks that are being executed.
                        • +
                        • options (LimiterOptions): The options of the limiter.
                        • +
                        +

                        In order to create more complex scenarios, the Limiter class allows to use pipe limiters to control the execution of tasks in a more complex way. This option allows, for example, to create several limiters to pull information from different sources, ensuring that this sources are not overloaded, and pipe them to a main limiter that will protect the own system from being overloaded.

                        +

                        Using schedule method:

                        +
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        });

                        const task1 = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        const task2 = new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        limiter.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        });

                        limiter.schedule(task1);
                        limiter.schedule(task2); +
                        + +

                        Using execute method:

                        +
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        });

                        const task1 = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        const task2 = new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        limiter.execute(task1).then(result => {
                        console.log('Task done', result);
                        });
                        limiter.execute(task2).then(result => {
                        console.log('Task done', result);
                        }); +
                        + +

                        The Scheduler class allows to schedule the execution of tasks based on resources and a polling times, this means periodically, controlling the execution of the tasks by the use of a Limiter instance per resource, piped with a Limiter for the Scheduler instance.

                        +

                        The Scheduler creates two types of cycles, a fast cycle and a slow cycle, per polling group and resource. The fast cycle is executed every time the polling group is reached, and the slow cycle is executed after slowCycleRatio fast cycles. The Scheduler class allows to control the execution of the tasks, and provides a set of metrics to monitor the execution of the tasks.

                        +

                        In order to create a new Scheduler instance, the constructor accepts the next parameters:

                        +
                          +
                        • name (string): The name of the scheduler.
                        • +
                        • options (SchedulerOptions): The options to configure the scheduler: +
                            +
                          • logger (Logger): The logger instance to use. If not provided, a default DebugLogger from the @mdf.js/logger package will be used with the name mdf:scheduler:${name}.
                          • +
                          • limiterOptions (LimiterOptions): The options to configure the limiter of the scheduler.
                          • +
                          • resources (ResourcesConfigObject): an object with an entry for each resource, where the key is the name of the resource, and the value is a ResourceConfigEntry with the following properties: +
                              +
                            • limiterOptions (LimiterOptions): The options to configure the limiter of the resource.
                            • +
                            • pollingGroups (object): A object with a entry for each polling group, where the key is the name of the group, and the value is a TaskBaseConfig array with the tasks to be executed in the group. The keys of this object should be of the type PollingGroup, this is a string with the format ${number}d, ${number}h, ${number}m, ${number}s, ${number}ms, where ${number} is the number of days, hours, minutes, seconds or milliseconds to wait between each polling. +The TaskBaseConfig could be a SingleTaskBaseConfig, a GroupTaskBaseConfig or a SequenceTaskBaseConfig object, with the following properties: +
                                +
                              • SingleTaskBaseConfig: +
                                  +
                                • task (TaskAsPromise): Promise to be executed.
                                • +
                                • taskArgs (any[]): Arguments to be passed to the task.
                                • +
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • +
                                +
                              • +
                              • GroupTaskBaseConfig: +
                                  +
                                • tasks (SingleTaskBaseConfig[]): Array of SingleTaskBaseConfig objects.
                                • +
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • +
                                +
                              • +
                              • SequenceTaskBaseConfig: +
                                  +
                                • pattern (SequencePattern): The pattern of the sequence, that defines the pre, post, main and finally tasks: +
                                    +
                                  • pre (SingleTaskBaseConfig[]): The tasks to be executed before the main task.
                                  • +
                                  • task (SingleTaskBaseConfig): The main task to be executed.
                                  • +
                                  • post (SingleTaskBaseConfig[]): The tasks to be executed after the main task, if the main task fails, the post tasks will not be executed.
                                  • +
                                  • finally (SingleTaskBaseConfig[]): The tasks to be executed at the end of the sequence, even if the main task fails.
                                  • +
                                  +
                                • +
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • +
                                +
                              • +
                              +
                            • +
                            +
                          • +
                          • slowCycleRatio (number): number of fast cycles to be executed before a slow cycle is executed. Default is 3.
                          • +
                          • cyclesOnStats (number): number of cycles to be included in the statistics. Default is 10.
                          • +
                          +
                        • +
                        +

                        The Scheduler has generic parameters in order to be typed:

                        +
                          +
                        • Result (Result): The type of the result of the tasks. If not provided, the result will be any.
                        • +
                        • Binding (Binding): The type of the object to bind the tasks to. If not provided, the binding will be any.
                        • +
                        • PollingGroups (PollingGroup): The available polling groups. If not provided, the polling groups will be DefaultPollingGroups: '1d', '12h', '6h', '6h', '1h', '30m', '15m', '10m', '5m', '1m', '30s', '10s', '5s'.
                        • +
                        +
                        import { Scheduler, SchedulerOptions } from '@mdf.js/tasks';

                        class MyClass {
                        constructor(private readonly resource: string) {};
                        task1(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        task2(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 3);
                        }, 1000);
                        });
                        }
                        }

                        const resource1 = new MyClass('resource1');
                        const resource2 = new MyClass('resource2');
                        type MyPollingGroups = '5m' | '1m';

                        const scheduler = new Scheduler<number, MyClass, MyPollingGroups>('myScheduler', {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        resources: {
                        resource1: {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource1.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource1.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        },
                        resource2: {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource2.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource2.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        },
                        },
                        }); +
                        + +

                        New resources can be added to the scheduler using the addResource or addResources methods, and deleted using the dropResource method, in all the cases the Scheduler should be stopped, in other case the method will throw an error. The resources can be cleared using the cleanup method.

                        +
                        scheduler.addResource('resource3', {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource2.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource2.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        });

                        scheduler.dropResource('resource3');
                        scheduler.cleanup(); +
                        + +

                        The Scheduler class allows to start and stop the scheduler, controlling the execution of the tasks:

                        +
                          +
                        • async start(): Promise<void>: Start the scheduler, allowing to process the tasks in the polling groups. If the scheduler is already started, it will not do anything.
                        • +
                        • async stop(): Promise<void>: Stop the scheduler, preventing to process the tasks in the polling groups. If the scheduler is already stopped, it will not do anything.
                        • +
                        • async close(): Promise<void>: Close the scheduler, stopping the scheduler and clearing the polling groups.
                        • +
                        +

                        Every time a task is executed, the done event is emitted with the following parameters:

                        +
                          +
                        • uuid: The unique identifier of the task instance.
                        • +
                        • result: The result of the task execution.
                        • +
                        • meta: The metadata of the task execution.
                        • +
                        • error: The error in case of failure.
                        • +
                        • resource: The name of the resource where the task was executed.
                        • +
                        +
                        scheduler.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash, resource: string) => {
                        console.log('Task done', uuid, result, meta, error, resource);
                        }); +
                        + +

                        The Scheduler class implements the Layer.App.Service interface, so it can be used with the @mdf.js/service-registry package to monitor the scheduler. The Scheduler class collect the following metrics for each resource and polling group:

                        +
                          +
                        • scanTime (Date): The date and time when the scan was performed.
                        • +
                        • cycles (number): The number of cycles performed.
                        • +
                        • overruns (number): The number of cycles with overruns.
                        • +
                        • consecutiveOverruns (number): The number of consecutive overruns.
                        • +
                        • averageCycleDuration (number): The average cycle duration in milliseconds.
                        • +
                        • maxCycleDuration (number): The maximum cycle duration in milliseconds.
                        • +
                        • minCycleDuration (number): The minimum cycle duration in milliseconds.
                        • +
                        • lastCycleDuration (number): The last cycle duration in milliseconds.
                        • +
                        • inFastCycleTasks (number): The number of tasks included on the regular cycle.
                        • +
                        • inSlowCycleTasks (number): The number of tasks included on the slow cycle. This cycle is executed after slowCycleRatio fast cycles.
                        • +
                        • inOffCycleTasks (number): The number of tasks included on the off cycle, these are not executed.
                        • +
                        • pendingTasks (number): The number of tasks in execution in this moment.
                        • +
                        +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +

                        @mdf.js/tasks

                        DoneListener

                        Other

                        <internal>
                        LimiterState
                        RETRY_STRATEGY
                        STRATEGY
                        TASK_STATE
                        Group
                        Limiter
                        PollingExecutor
                        Scheduler
                        Sequence
                        Single
                        TaskHandler
                        ConsolidatedLimiterOptions
                        GroupTaskBaseConfig
                        LimiterOptions
                        MetaData
                        PollingManagerOptions
                        PollingStats
                        QueueOptions
                        ResourceConfigEntry
                        ResourcesConfigObject
                        SchedulerOptions
                        SequencePattern
                        SequenceTaskBaseConfig
                        SingleTaskBaseConfig
                        TaskOptions
                        WellIdentifiedTaskOptions
                        DefaultPollingGroups
                        MetricsDefinitions
                        PollingGroup
                        RetryStrategy
                        ScanMetricsDefinitions
                        Strategy
                        TaskBaseConfig
                        TaskMetricsDefinitions
                        TaskState
                        STRATEGIES
                        TASK_STATES
                        diff --git a/docs/modules/_mdf.js_utils.html b/docs/modules/_mdf.js_utils.html new file mode 100644 index 00000000..e9024e49 --- /dev/null +++ b/docs/modules/_mdf.js_utils.html @@ -0,0 +1,176 @@ +@mdf.js/utils | @mdf.js

                        Module @mdf.js/utils

                        @mdf.js/utils

                        Node Version +Typescript Version +Known Vulnerabilities +Documentation

                        + +

                        +

                        + netin +
                        +

                        +

                        Mytra Development Framework - @mdf.js/utils

                        +
                        Collection of tools useful for several different tasks within the @mdf.js ecosystem
                        + +
                        + +

                        The @mdf.js/utils module is a collection of tools useful for several different tasks within the @mdf.js ecosystem. It is a collection of utilities that are used in different parts of the framework, such as the API, the CLI, the documentation...

                        +

                        The list of utilities are:

                        +
                          +
                        • retry and retryBind: A function that allows you to retry a promise a certain number of times before giving up.
                        • +
                        • prettyMS: A function that converts milliseconds to a human-readable format.
                        • +
                        • loadFile: A function that loads a file from the file system, logging the process.
                        • +
                        • findNodeModule: A function that finds a node module in the file system.
                        • +
                        • escapeRegExp: A function that escapes a string to be used in a regular expression.
                        • +
                        • coerce: function for data type coercion, specially useful for environment variables and configuration files.
                        • +
                        • camelCase: function for converting strings to camelCase.
                        • +
                        • cycle: function for managing circular references in objects.
                        • +
                        • formatEnv: functions for formatting environment variables.
                        • +
                        • mock: functions for mocking objects, specially useful for testing in Jest.
                        • +
                        +

                        To install the @mdf.js/utils module, you can use the following commands:

                        +
                          +
                        • NPM
                        • +
                        +
                        npm install @mdf.js/utils
                        +
                        + +
                          +
                        • Yarn
                        • +
                        +
                        yarn add @mdf.js/utils
                        +
                        + +

                        The retry and retryBind functions are used to retry a promise a certain number of times before giving up. The difference between both functions is that the retryBind can bind the context of the promise to a concrete object.

                        +

                        Both functions have similar signatures:

                        +
                          +
                        • retry: retry<T>(task: TaskAsPromise<T>, funcArgs: TaskArguments, options: RetryOptions): Promise<T>.
                        • +
                        • retryBind: retryBind<T, U>(task: TaskAsPromise<T>, bindTo: U, funcArgs: TaskArguments, options: RetryOptions): Promise<T>.
                        • +
                        +

                        The common parameters are between both functions are:

                        +
                          +
                        • task (TaskAsPromise<T>): The task to be executed. TaskAsPromise<T> is alias type for (...args: TaskArguments) => Promise<T>.
                        • +
                        • funcArgs (TaskArguments): The arguments to be passed to the task. TaskArguments is an alias type for any[].
                        • +
                        • options (RetryOptions): The options for the retry. RetryOptions is an interface with the following properties: +
                            +
                          • logger (LoggerFunction): The logger function used for logging retry attempts. LoggerFunction is an alias type for (error: Crash | Multi | Boom) => void. Crash, Multi, and Boom are errors defined in the @mdf.js/crash.
                          • +
                          • waitTime (number): The time to wait between retry attempts, in milliseconds. Default is 1000.
                          • +
                          • maxWaitTime (number): The maximum time to wait between retry attempts, in milliseconds. Default is 15000.
                          • +
                          • abortSignal (AbortSignal): The signal to be used to interrupt the retry process. Default is null.
                          • +
                          • attempts (number): The maximum number of retry attempts. Default is Number.MAX_SAFE_INTEGER.
                          • +
                          • timeout (number): Timeout for each try. Default is undefined.
                          • +
                          • interrupt (() => boolean): A function that determines whether to interrupt the retry process. Should return true to interrupt, false otherwise. Default is undefined. Deprecated. Use abortSignal instead.
                          • +
                          +
                        • +
                        +

                        The extra parameter for the retryBind function is:

                        +
                          +
                        • bindTo (U): The object to bind the context of the promise to.
                        • +
                        +

                        Simple task:

                        +
                        import { retry, retryBind } from '@mdf.js/utils';
                        import { Logger } from '@mdf.js/logger';

                        const logger: Logger = new Logger();

                        const task = async (a: number, b: number) => {
                        if (Math.random() < 0.5) {
                        throw new Error('Random error');
                        }
                        return a + b;
                        };

                        const funcArgs = [1, 2];
                        const options = {
                        logger: logger.crash,
                        waitTime: 1000,
                        maxWaitTime: 15000,
                        attempts: 5,
                        timeout: 5000,
                        };

                        retry(task, [1, 2], options).then(console.log).catch(console.error); +
                        + +

                        Aborting the retry process can be done using an AbortSignal:

                        +
                        import { retry } from '@mdf.js/utils';
                        import { Logger } from '@mdf.js/logger';

                        const logger: Logger = new Logger();
                        const controller = new AbortController();

                        class MyContext {
                        public c = 10;
                        public task = async (a: number, b: number) => {
                        if (Math.random() < 0.5) {
                        throw new Error('Random error');
                        }
                        return a + b;
                        };
                        }

                        const context = new MyContext();

                        const options = {
                        logger: logger.crash,
                        waitTime: 1000,
                        maxWaitTime: 15000,
                        attempts: 5,
                        timeout: 5000,
                        abortSignal: controller.signal,
                        };

                        setTimeout(() => controller.abort(), 3000);
                        retryBind(context.task, context, [1, 2], options).then(console.log).catch(console.error); +
                        + +

                        The prettyMS function is used to convert milliseconds to a human-readable format. It has the following signature:

                        +
                          +
                        • prettyMS: (ms: number): string.
                        • +
                        +
                        import { prettyMS } from '@mdf.js/utils';

                        console.log(prettyMS(1000)); // 1s
                        console.log(prettyMS(1000 * 60)); // 1m
                        console.log(prettyMS(1000 * 60 * 60)); // 1h
                        console.log(prettyMS(1000 * 60 * 60 * 24)); // 1d +
                        + +

                        The loadFile function is used to load a file from the file system, logging the process. It has the following signature:

                        +
                          +
                        • loadFile: (path: string, logger?: LoggerInstance): Buffer | undefined. The logger parameter is optional and is used to log the process, it should be an instance of the LoggerInstance class from the @mdf.js/logger package or a simple object with a debug method that accepts a string.
                        • +
                        +
                        import { loadFile } from '@mdf.js/utils';

                        const logger = {
                        debug: (message: string) => console.log(message),
                        };
                        const file = loadFile('path/to/file', logger); +
                        + +

                        The findNodeModule function is used to find a node module in the file system. It has the following signature:

                        +
                          +
                        • findNodeModule: (module: string, dir?: string): string | undefined. The dir parameter is optional and is used to specify the current working directory, default is __dirname, this means that the search will start from the own module.
                        • +
                        +
                        import { findNodeModule } from '@mdf.js/utils';

                        const modulePath = findNodeModule('module-name'); +
                        + +

                        The escapeRegExp function is used to get the source of a regular expression pattern and escape it. It has the following signature:

                        +
                          +
                        • escapeRegExp: (regex: RexExp): string. The regex parameter is the regular expression to escape.
                        • +
                        +
                        import { escapeRegExp } from '@mdf.js/utils';

                        const escaped = escapeRegExp(/([.*+?^=!:${}()|\[\]\/\\])/g);
                        console.log(escaped); // \(\[\.\*\+\?\^\=\!\:\$\{\}\(\)\|\[\]\/\\]\) +
                        + +

                        The coerce function is used for data type coercion, specially useful for environment variables and configuration files. It has the following signature:

                        +
                          +
                        • coerce: <T extends Coerceable>(env: string | undefined, alternative?: T): T | undefined. The env parameter is the value to coerce, and the alternative parameter is the default value to return if the coercion fails. The Coerceable type is an alias type for number | boolean | Record<string, any> | any[] | null.
                        • +
                        +
                        import { coerce } from '@mdf.js/utils';

                        // process.env['MY_ENV_VAR'] = '1';
                        const asNumber = coerce<number>(process.env['MY_ENV_VAR'], 10); // Coerce to number, default to 10
                        // process.env['MY_ENV_VAR'] = 'true' or 'false';
                        const asBoolean = coerce<boolean>(process.env['MY_ENV_VAR'], true); // Coerce to boolean, default to true
                        // process.env['MY_ENV_VAR'] = '{"a": 1}';
                        const asObject = coerce<Record<string, any>>(process.env['MY_ENV_VAR'], { a: 1 }); // Coerce to object, default to { a: 1 }
                        // process.env['MY_ENV_VAR'] = '[1,2,3]';
                        const asArray = coerce<any[]>(process.env['MY_ENV_VAR'], [1, 2, 3]); // Coerce to array, default to [1, 2, 3]
                        // process.env['MY_ENV_VAR'] = 'null' or 'NULL';
                        const asNull = coerce(process.env['MY_ENV_VAR']); // Coerce to null +
                        + +

                        The camelCase function is used to convert strings to camelCase. It has the following signature:

                        +
                          +
                        • camelCase: (input: string | string[], options?: Options): string. The input parameter is the string or array of strings to convert, and the options parameter is an object with the following properties: +
                            +
                          • pascalCase (boolean): Convert to PascalCase. foo-bar -> FooBar. Default is false.
                          • +
                          • preserveConsecutiveUppercase (boolean): Preserve consecutive uppercase characters. foo-BAR -> fooBAR. Default is false.
                          • +
                          • locale (string | string[]): The locale parameter indicates the locale to be used to convert to upper/lower case according to any locale-specific case mappings. If multiple locales are given in an array, the best available locale is used. Setting locale: false ignores the platform locale and uses the Unicode Default Case Conversion algorithm. Default: The host environment’s current locale.
                          • +
                          +
                        • +
                        +
                        import { camelCase } from 'camelCase';

                        camelCase('foo-bar');
                        //=> 'fooBar'
                        camelCase('foo-bar', { pascalCase: true });
                        //=> 'FooBar'
                        camelCase('foo-BAR', { preserveConsecutiveUppercase: true });
                        //=> 'fooBAR'
                        camelCase('lorem-ipsum', { locale: 'en-US' });
                        //=> 'loremIpsum'
                        camelCase('lorem-ipsum', { locale: 'tr-TR' });
                        //=> 'loremİpsum'
                        camelCase('lorem-ipsum', { locale: ['en-US', 'en-GB'] });
                        //=> 'loremIpsum'
                        camelCase('lorem-ipsum', { locale: ['tr', 'TR', 'tr-TR'] });
                        //=> 'loremİpsum' +
                        + +

                        The deCycle and retroCycle functions are used to manage circular references in objects. The deCycle function is used to remove circular references from an object, and the retroCycle function is used to restore circular references to an object. They have the following signatures:

                        +
                          +
                        • deCycle: (object: any, replacer?: (value: any) => any): any. The object parameter is the object to remove circular references from, and the replacer parameter is a function that replaces circular references with a placeholder. Default is undefined.
                        • +
                        • retroCycle: (obj: any): any. The obj parameter is the object to restore circular references to.
                        • +
                        +
                        import { deCycle, retroCycle } from '@mdf.js/utils';

                        const obj = { a: 1 };
                        obj.b = obj;

                        const deCycled = deCycle(obj);
                        console.log(deCycled); // { a: 1, b: '$' }

                        const retroCycled = retroCycle(deCycled);
                        console.log(retroCycled); // { a: 1, b: [Circular] } +
                        + +

                        The formatEnv function is used to read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options. It has the following signatures:

                        +
                          +
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(): T. Read environment variables (process.env) and return an object with the values sanitized and the keys formatted.
                        • +
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string): T. Read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted.
                        • +
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string, options: Partial<ReadEnvOptions>): T. Read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options. The ReadEnvOptions type is an interface with the following properties: +
                            +
                          • separator (string): The separator to use for nested keys. Default is __.
                          • +
                          • format ('camelcase' | 'pascalcase' | 'lowercase' | 'uppercase'): The format to use for the keys. Default is 'camelcase'.
                          • +
                          • includePrefix (boolean): Whether to include the prefix in the keys. Default is false.
                          • +
                          +
                        • +
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string, options: Partial<ReadEnvOptions>, source: Record<string, string | undefined>): T. Process a source, encoded as an environment variables file, filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options.
                        • +
                        +
                        import { formatEnv } from '@mdf.js/utils';

                        process.env['MY_OWN_TEST'] = 'test';

                        const env = {
                        EXAMPLE_OBJECT: '{"prop": "value"}',
                        EXAMPLE_ARRAY: '[1,2,3, "string", {"prop": "value"}, 5.2]',
                        EXAMPLE_INVALID_OBJECT: '{"prop": }"value"}',
                        EXAMPLE_INVALID_ARRAY: '[1,2,3, "string", ]{"prop": "value"}, 5.2]',
                        EXAMPLE_TRUE: 'true',
                        EXAMPLE_FALSE: 'false',
                        EXAMPLE_INT: '5',
                        EXAMPLE_NEGATIVE_INT: '-11',
                        EXAMPLE_FLOAT: '5.2456',
                        EXAMPLE_NEGATIVE_FLOAT: '-2.4567',
                        EXAMPLE_INT_ZERO: '0',
                        EXAMPLE_FLOAT_ZERO: '0.00',
                        EXAMPLE_NEGATIVE_INT_ZERO: '-0',
                        EXAMPLE_NEGATIVE_FLOAT_ZERO: '-0.00',
                        EXAMPLE_STRING: 'example',
                        EXAMPLE_DEEP__OBJECT__PROPERTY: 'value',
                        EXAMPLE_NOT_SHOULD_BE_SANITIZED: 5,
                        };

                        console.log(formatEnv()); // { myOwnTest: 'test' }

                        console.log(
                        formatEnv('EXAMPLE', { separator: '__', format: 'camelcase', includePrefix: false }, env)
                        );
                        // {
                        // object: { prop: 'value' },
                        // array: [1, 2, 3, 'string', { prop: 'value' }, 5.2],
                        // invalidObject: '{"prop": }"value"}',
                        // invalidArray: '[1,2,3, "string", ]{"prop": "value"}, 5.2]',
                        // true: true,
                        // false: false,
                        // int: 5,
                        // negativeInt: -11,
                        // float: 5.2456,
                        // negativeFloat: -2.4567,
                        // intZero: 0,
                        // floatZero: 0,
                        // negativeIntZero: -0,
                        // negativeFloatZero: -0,
                        // string: 'example',
                        // deep: {
                        // object: {
                        // property: 'value',
                        // },
                        // },
                        // notShouldBeSanitized: 5,
                        // } +
                        + +

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        +
                        
                        +
                        + +

                        Interfaces

                        ReadEnvOptions
                        RetryOptions

                        Type Aliases

                        Coercible
                        Format
                        FormatFunction
                        LoggerFunction
                        LoggerInstance
                        TaskArguments
                        TaskAsPromise

                        Variables

                        MAX_WAIT_TIME
                        WAIT_TIME

                        Functions

                        coerce
                        deCycle
                        escapeRegExp
                        findNodeModule
                        formatEnv
                        loadFile
                        prettyMS
                        retroCycle
                        retry
                        retryBind
                        wrapOnRetry
                        diff --git a/docs/modules/_mdf_js_amqp_provider.Receiver.html b/docs/modules/_mdf_js_amqp_provider.Receiver.html deleted file mode 100644 index e3958a0c..00000000 --- a/docs/modules/_mdf_js_amqp_provider.Receiver.html +++ /dev/null @@ -1,6 +0,0 @@ -Receiver | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_amqp_provider.Sender.html b/docs/modules/_mdf_js_amqp_provider.Sender.html deleted file mode 100644 index 01019faf..00000000 --- a/docs/modules/_mdf_js_amqp_provider.Sender.html +++ /dev/null @@ -1,3 +0,0 @@ -Sender | @mdf.js

                        Index

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_amqp_provider.html b/docs/modules/_mdf_js_amqp_provider.html deleted file mode 100644 index 29fe8157..00000000 --- a/docs/modules/_mdf_js_amqp_provider.html +++ /dev/null @@ -1,163 +0,0 @@ -@mdf.js/amqp-provider | @mdf.js

                        Module @mdf.js/amqp-provider

                        @mdf.js/amqp-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/amqp-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        AMQP provider for @mdf.js based on rhea.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/amqp-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/amqp-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        In this module there are implemented two providers:

                        -
                          -
                        • -

                          The consumer (Receiver), that wraps the rhea-promise Receiver, which wraps the rhea Receiver class.

                          -
                          import { Receiver } from '@mdf.js/amqp-provider';

                          const ownReceiver = Receiver.Factory.create({
                          name: `myAMQPReceiverName`,
                          config: {...}, //rhea - AMQP CommonConnectionOptions
                          logger: myLoggerInstance,
                          useEnvironment: true,
                          }); -
                          - -
                            -
                          • -

                            Defaults:

                            -
                            {
                            // ... Common client options, see below
                            receiver_options: {
                            name: process.env['NODE_APP_INSTANCE'] ||'mdf-amqp',
                            rcv_settle_mode: 0,
                            credit_window: 0,
                            autoaccept: false,
                            autosettle: true,
                            }
                            } -
                            - -
                          • -
                          • -

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            -
                            {
                            // ... Common client options, see below
                            receiver_options: {
                            name: process.env['CONFIG_AMQP_RECEIVER_NAME'],
                            rcv_settle_mode: process.env['CONFIG_AMQP_RECEIVER_SETTLE_MODE'], // coerced to number
                            credit_window: process.env['CONFIG_AMQP_RECEIVER_CREDIT_WINDOW'], // coerced to number
                            autoaccept: process.env['CONFIG_AMQP_RECEIVER_AUTO_ACCEPT'], // coerced to boolean
                            autosettle: process.env['CONFIG_AMQP_RECEIVER_AUTO_SETTLE'], // coerced to boolean
                            }
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          The producer (Sender) that wraps the rhea-promise AwaitableSender class.

                          -
                          import { Sender } from '@mdf.js/amqp-provider';

                          const ownSender = Sender.Factory.create({
                          name: `myAMQPSenderName`,
                          config: {...}, //rhea - AMQP CommonConnectionOptions
                          logger: myLoggerInstance,
                          useEnvironment: true,
                          }); -
                          - -
                            -
                          • -

                            Defaults:

                            -
                            {
                            // ... Common client options, see below
                            sender_options: {
                            name: process.env['NODE_APP_INSTANCE'] ||'mdf-amqp',
                            snd_settle_mode: 2,
                            autosettle: true,
                            target: {},
                            }
                            } -
                            - -
                          • -
                          • -

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            -
                            {
                            // ... Common client options, see below
                            sender_options: {
                            name: process.env['CONFIG_AMQP_SENDER_NAME'],
                            snd_settle_mode: process.env['CONFIG_AMQP_SENDER_SETTLE_MODE'], // coerced to number
                            autosettle: process.env['CONFIG_AMQP_SENDER_AUTO_SETTLE'], // coerced to boolean
                            }
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          Common client options:

                          -
                            -
                          • -

                            Defaults:

                            -
                            {
                            username: 'mdf-amqp',
                            host: '127.0.0.1',
                            port: 5672,
                            transport: 'tcp',
                            container_id: process.env['NODE_APP_INSTANCE'] || 'mdf-amqp',
                            reconnect: 5000,
                            initial_reconnect_delay: 30000,
                            max_reconnect_delay: 10000,
                            non_fatal_errors: ['amqp:connection:forced'],
                            idle_time_out: 5000,
                            reconnect_limit: Number.MAX_SAFE_INTEGER,
                            keepAlive: true,
                            keepAliveInitialDelay: 2000,
                            timeout: 10000,
                            all_errors_non_fatal: true,
                            } -
                            - -
                          • -
                          • -

                            Environment: remember to set the useEnvironment flag to true to use these environment variables.

                            -
                            {
                            username: process.env['CONFIG_AMQP_USER_NAME'],
                            password: process.env['CONFIG_AMQP_PASSWORD'],
                            host: process.env['CONFIG_AMQP_HOST'],
                            hostname: process.env['CONFIG_AMQP_HOSTNAME'],
                            port: process.env['CONFIG_AMQP_PORT'], // coerced to number
                            transport: process.env['CONFIG_AMQP_TRANSPORT'],
                            container_id: process.env['CONFIG_AMQP_CONTAINER_ID'],
                            id: process.env['CONFIG_AMQP_ID'],
                            reconnect: process.env['CONFIG_AMQP_RECONNECT'], // coerced to number
                            reconnect_limit: process.env['CONFIG_AMQP_RECONNECT_LIMIT'], // coerced to number
                            initial_reconnect_delay: process.env['CONFIG_AMQP_INITIAL_RECONNECT_DELAY'], // coerced to number
                            max_reconnect_delay: process.env['CONFIG_AMQP_MAX_RECONNECT_DELAY'], // coerced to number
                            max_frame_size: process.env['CONFIG_AMQP_MAX_FRAME_SIZE'], // coerced to number
                            non_fatal_errors: process.env['CONFIG_AMQP_NON_FATAL_ERRORS'], // coerced to array from string separated by ','
                            key: process.env['CONFIG_AMQP_CLIENT_KEY_PATH'], // The file will be read and the content will be used as the key
                            cert: process.env['CONFIG_AMQP_CLIENT_CERT_PATH'], // The file will be read and the content will be used as the cert
                            ca: process.env['CONFIG_AMQP_CA_PATH'], // The file will be read and the content will be used as the CA
                            requestCert: process.env['CONFIG_AMQP_REQUEST_CERT'], // coerced to boolean
                            rejectUnauthorized: process.env['CONFIG_AMQP_REJECT_UNAUTHORIZED'], // coerced to boolean
                            idle_time_out: process.env['CONFIG_AMQP_IDLE_TIME_OUT'], // coerced to number
                            keepAlive: process.env['CONFIG_AMQP_KEEP_ALIVE'], // coerced to boolean
                            keepAliveInitialDelay: process.env['CONFIG_AMQP_KEEP_ALIVE_INITIAL_DELAY'], // coerced to number
                            timeout: process.env['CONFIG_AMQP_TIMEOUT'], // coerced to number
                            all_errors_non_fatal: process.env['CONFIG_AMQP_ALL_ERRORS_NON_FATAL'], // coerced to boolean
                            }; -
                            - -
                          • -
                          -
                        • -
                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the status of the AMQP connection -
                            -
                          • observedValue: Actual state of the consumer/producer provider instance [error, running, stopped].
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          • output: in case of error state (status fail), the error message is shown.
                          • -
                          -
                        • -
                        • credits: Checks the credits of the AMQP connection -
                            -
                          • observedValue: Actual number of credits in the consumer/producer instance.
                          • -
                          • observedUnit: credits.
                          • -
                          • status: pass if the number of credits is greater than 0, warn otherwise.
                          • -
                          • output: No credits available if the number of credits is 0.
                          • -
                          -
                        • -
                        -
                        {
                        "[mdf-amqp:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "service",
                        "output": undefined
                        }
                        ],
                        "[mdf-amqp:credits]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": 10,
                        "observedUnit": "credits",
                        "output": undefined
                        }
                        ]
                        } -
                        - -
                          -
                        • CONFIG_AMQP_SENDER_NAME (default: NODE_APP_INSTANCE || `mdf-amqp`): The name of the link. This should be unique for the container. If not specified a unique name is generated.
                        • -
                        • CONFIG_AMQP_SENDER_SETTLE_MODE (default: 2): It specifies the sender settle mode with following possible values: - 0 - "unsettled" - The sender will send all deliveries initially unsettled to the receiver. - 1 - "settled" - The sender will send all deliveries settled to the receiver. - 2 - "mixed" - (default) The sender MAY send a mixture of settled and unsettled deliveries to the receiver.
                        • -
                        • CONFIG_AMQP_SENDER_AUTO_SETTLE (default: true): Whether sent messages should be automatically settled once the peer settles them.
                        • -
                        • CONFIG_AMQP_RECEIVER_NAME (default: NODE_APP_INSTANCE || `mdf-amqp`): The name of the link. This should be unique for the container. If not specified a unique name is generated.
                        • -
                        • CONFIG_AMQP_RECEIVER_SETTLE_MODE (default: 0): It specifies the receiver settle mode with following possible values: - 0 - "first" - The receiver will spontaneously settle all incoming transfers. - 1 - "second" - The receiver will only settle after sending the disposition to the sender and receiving a disposition indicating settlement of the delivery from the sender.
                        • -
                        • CONFIG_AMQP_RECEIVER_CREDIT_WINDOW (default: 0): A "prefetch" window controlling the flow of messages over this receiver. Defaults to 1000 if not specified. A value of 0 can be used to turn off automatic flow control and manage it directly.
                        • -
                        • CONFIG_AMQP_RECEIVER_AUTO_ACCEPT (default: false): Whether received messages should be automatically accepted.
                        • -
                        • CONFIG_AMQP_RECEIVER_AUTO_SETTLE (default: true): Whether received messages should be automatically settled once the remote settles them.
                        • -
                        • CONFIG_AMQP_USER_NAME (default: 'mdf-amqp'): User name for the AMQP connection
                        • -
                        • CONFIG_AMQP_PASSWORD (default: undefined): The secret key to be used while establishing the connection
                        • -
                        • CONFIG_AMQP_HOST (default: undefined): The hostname of the AMQP server
                        • -
                        • CONFIG_AMQP_HOSTNAME (default: 127.0.0.1): The hostname presented in open frame, defaults to host.
                        • -
                        • CONFIG_AMQP_PORT (default: 5672): The port of the AMQP server
                        • -
                        • CONFIG_AMQP_TRANSPORT (default: 'tcp'): The transport option. This is ignored if connection_details is set.
                        • -
                        • NODE_APP_INSTANCE (default: 'tcp'): The transport option. This is ignored if connection_details is set.
                        • -
                        • CONFIG_AMQP_CONTAINER_ID (default: process.env['NODE_APP_INSTANCE'] || `mdf-amqp`): The id of the source container. If not provided then this will be the id (a guid string) of the assocaited container object. When this property is provided, it will be used in the open frame to let the peer know about the container id. However, the associated container object would still be the same container object from which the connection is being created. The "container\_id" is how the peer will identify the 'container' the connection is being established from. The container in AMQP terminology is roughly analogous to a process. Using a different container id on connections from the same process would cause the peer to treat them as coming from distinct processes.
                        • -
                        • CONFIG_AMQP_ID (default: undefined): A unique name for the connection. If not provided then this will be a string in the following format: "connection-<counter>".
                        • -
                        • CONFIG_AMQP_RECONNECT (default: 5000): If true (default), the library will automatically attempt to reconnect if disconnected. If false, automatic reconnect will be disabled. If it is a numeric value, it is interpreted as the delay between reconnect attempts (in milliseconds).
                        • -
                        • CONFIG_AMQP_RECONNECT_LIMIT (default: undefined): Maximum number of reconnect attempts. Applicable only when reconnect is true.
                        • -
                        • CONFIG_AMQP_INITIAL_RECONNECT_DELAY (default: 30000): Time to wait in milliseconds before attempting to reconnect. Applicable only when reconnect is true or a number is provided for reconnect.
                        • -
                        • CONFIG_AMQP_MAX_RECONNECT_DELAY (default: 10000): Maximum reconnect delay in milliseconds before attempting to reconnect. Applicable only when reconnect is true.
                        • -
                        • CONFIG_AMQP_MAX_FRAME_SIZE (default: 4294967295): The largest frame size that the sending peer is able to accept on this connection.
                        • -
                        • CONFIG_AMQP_NON_FATAL_ERRORS (default: ['amqp:connection:forced']): An array of error conditions which if received on connection close from peer should not prevent reconnect (by default this only includes "amqp:connection:forced").
                        • -
                        • CONFIG_AMQP_NON_FATAL_ERRORS (default: ['amqp:connection:forced']): An array of error conditions which if received on connection close from peer should not prevent reconnect (by default this only includes "amqp:connection:forced").
                        • -
                        • CONFIG_AMQP_CA_PATH (default: undefined): The path to the CA certificate file
                        • -
                        • CONFIG_AMQP_CLIENT_CERT_PATH (default: undefined): The path to the client certificate file
                        • -
                        • CONFIG_AMQP_CLIENT_KEY_PATH (default: undefined): The path to the client key file
                        • -
                        • CONFIG_AMQP_REQUEST_CERT (default: false): If true the server will request a certificate from clients that connect and attempt to verify that certificate. Defaults to false.
                        • -
                        • CONFIG_AMQP_REJECT_UNAUTHORIZED (default: true): If true the server will reject any connection which is not authorized with the list of supplied CAs. This option only has an effect if requestCert is true.
                        • -
                        • CONFIG_AMQP_IDLE_TIME_OUT (default: 5000): The maximum period in milliseconds between activity (frames) on the connection that is desired from the peer. The open frame carries the idle-time-out field for this purpose. To avoid spurious timeouts, the value in idle_time_out is set to be half of the peer’s actual timeout threshold.
                        • -
                        • CONFIG_AMQP_KEEP_ALIVE (default: true): If true the server will send a keep-alive packet to maintain the connection alive.
                        • -
                        • CONFIG_AMQP_KEEP_ALIVE_INITIAL_DELAY (default: 2000): The initial delay in milliseconds for the keep-alive packet.
                        • -
                        • CONFIG_AMQP_TIMEOUT (default: 10000): The time in milliseconds to wait for the connection to be established.
                        • -
                        • CONFIG_AMQP_ALL_ERRORS_NON_FATAL (default: true): Determines if rhea's auto-reconnect should attempt reconnection on all fatal errors
                        • -
                        • NODE_APP_INSTANCE (default: undefined): Used as default container id, receiver name, sender name, etc. in cluster configurations.
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_core.Health.html b/docs/modules/_mdf_js_core.Health.html deleted file mode 100644 index 3d2de266..00000000 --- a/docs/modules/_mdf_js_core.Health.html +++ /dev/null @@ -1,13 +0,0 @@ -Health | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Enumerations

                        Interfaces

                        Type Aliases

                        Variables

                        Functions

                        diff --git a/docs/modules/_mdf_js_core.Jobs.html b/docs/modules/_mdf_js_core.Jobs.html deleted file mode 100644 index 479c4f2a..00000000 --- a/docs/modules/_mdf_js_core.Jobs.html +++ /dev/null @@ -1,15 +0,0 @@ -Jobs | @mdf.js
                        diff --git a/docs/modules/_mdf_js_core.Layer.App.html b/docs/modules/_mdf_js_core.Layer.App.html deleted file mode 100644 index 97c24d93..00000000 --- a/docs/modules/_mdf_js_core.Layer.App.html +++ /dev/null @@ -1,6 +0,0 @@ -App | @mdf.js
                        diff --git a/docs/modules/_mdf_js_core.Layer.Provider.html b/docs/modules/_mdf_js_core.Layer.Provider.html deleted file mode 100644 index 841f829d..00000000 --- a/docs/modules/_mdf_js_core.Layer.Provider.html +++ /dev/null @@ -1,11 +0,0 @@ -Provider | @mdf.js
                        diff --git a/docs/modules/_mdf_js_core.Layer.html b/docs/modules/_mdf_js_core.Layer.html deleted file mode 100644 index 817ab040..00000000 --- a/docs/modules/_mdf_js_core.Layer.html +++ /dev/null @@ -1,4 +0,0 @@ -Layer | @mdf.js

                        Index

                        Namespaces

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_core.html b/docs/modules/_mdf_js_core.html deleted file mode 100644 index d273c368..00000000 --- a/docs/modules/_mdf_js_core.html +++ /dev/null @@ -1,489 +0,0 @@ -@mdf.js/core | @mdf.js

                        Module @mdf.js/core

                        @mdf.js/core

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/core

                        -
                        Core module with shared components for resource management and instrumentation API
                        - -
                        - -

                        The @mdf.js/core module is a set of types, interfaces, and classes that standardize the way in which the state of the resources managed by the applications developed with @mdf.js is reported. This module is part of the @mdf.js ecosystem, which aims to provide a set of tools and libraries that facilitate the development of applications in Node.js, especially those that require a high level of observability and monitoring.

                        -

                        The @mdf.js/core module is composed of the following elements:

                        -
                          -
                        • The Health interface, which is a set of types and interfaces that standardize the way in which the state of the resources is reported.
                        • -
                        • The Layer namespace, which contains the following elements: -
                            -
                          • The App interface, which is a set of types and interfaces that standardize the components of an application from the observability point of view.
                          • -
                          • The Provider API, which allows for the instrumentation of resource providers (databases, publish/subscribe services, etc.) so they can be managed in a standardized way within the @mdf.js API, especially in terms of observability, configuration management, and resource provider state management.
                          • -
                          -
                        • -
                        • The Jobs API, which allows for the creation and management of jobs in a standardized way within the @mdf.js API.
                        • -
                        -

                        To install the @mdf.js/core module, you can use the following commands:

                        -
                          -
                        • npm
                        • -
                        -
                        npm install @mdf.js/core
                        -
                        - -
                          -
                        • yarn
                        • -
                        -
                        yarn add @mdf.js/core
                        -
                        - -

                        The Health interface is a set of types and interfaces that standardize the way in which the state of the resources managed by the Provider is reported. The Health interface is composed of the following types and interfaces:

                        -
                          -
                        • -

                          Status: a type that represents the status of a resource, which can be one of the following values:

                          -
                            -
                          • pass: indicates that the resource is in a normal operating state.
                          • -
                          • fail: indicates that the resource is in an error state.
                          • -
                          • warn: indicates that the resource is in a warning state.
                          • -
                          -
                        • -
                        • -

                          Check<T>: an interface that defines the structure of a check object, with the following properties:

                          -
                            -
                          • componentId: a unique identifier for an instance of a specific sub-component/dependency of a service in UUID v4 format. Multiple objects with the same componentId may appear in the details if they are from different nodes.
                          • -
                          • componentType: an optional string that SHOULD be present if componentName is present. It indicates the type of the component, which could be a pre-defined value from the spec (such as component, datastore, or system), a common and standard term from a well-known source (like schema.org, IANA, or microformats), or a URI indicating extra semantics and processing rules.
                          • -
                          • observedValue: an optional property that could be any valid JSON value (such as a string, number, object, array, or literal). The type is referenced by T in the interface definition.
                          • -
                          • observedUnit: an optional string that SHOULD be present if metricValue is present. It could be a common and standard term from a well-known source or a URI indicating extra semantics and processing rules.
                          • -
                          • status: a value of type Status indicating whether the service status is acceptable or not.
                          • -
                          • affectedEndpoints: an optional array of strings containing URI Templates as defined by [RFC6570], indicating which particular endpoints are affected by the check.
                          • -
                          • time: an optional string indicating the date-time, in ISO8601 format, at which the reading of the metricValue was recorded.
                          • -
                          • output: an optional property containing raw error output in case of “fail” or “warn” states. This field SHOULD be omitted for “pass” state.
                          • -
                          • links: an optional object containing link relations and URIs [RFC3986] for external links that may contain more information about the health of the endpoint. This includes potentially a “self” link, which may be used by clients to check health via HTTP response code.
                          • -
                          • ... and any other property that the Provider developer considers necessary to provide more information about the check.
                          • -
                          -
                        • -
                        • -

                          Checks: The "checks" object within the health check model allows for the representation of the health status of various logical sub-components of a service. This flexible structure is designed to accommodate the complexities of modern distributed systems, where each component may consist of multiple nodes, each potentially exhibiting a different health status. Here's a breakdown of how the "checks" object is structured and the semantics of its keys and values:

                          -
                            -
                          • Each key in the "checks" object represents a logical sub-component of the service. The uniqueness of each key ensures that the health status of each sub-component can be individually assessed and reported.
                          • -
                          • The value associated with each key is an array of Check objects. This array accommodates scenarios where a single logical sub-component is supported by multiple nodes. For single-node sub-components, or when the distinction between nodes is not relevant, a single-element array is used for consistency.
                          • -
                          • The key for each sub-component is a unique string within the "details" section of the health check model. It may consist of two parts, separated by a colon (:): {componentName}:{metricName}. The structure of these keys is as follows: -
                              -
                            • componentName: This part of the key provides a human-readable identifier for the component. It must not contain a colon, as the colon serves as the delimiter between the component name and the metric name.
                            • -
                            • metricName: This part specifies the particular metric for which the health status is reported. Like the component name, it must not contain a colon. The metric name can be a pre-defined value specified by the health check model (such as "utilization," "responseTime," "connections," or "uptime"), a common term from a recognized standard or organization (like schema.org, IANA, or microformats), or a URI that conveys additional semantics and processing rules associated with the metric.
                            • -
                            -
                          • -
                          -

                          The Checks type is defined to capture this structure, where each entry in the object maps to an array of Check objects, allowing for a detailed and nuanced representation of the health status across different parts of a service and its underlying infrastructure.

                          -
                          export type Checks<T = any> = {
                          [entry in CheckEntry]: Check<T>[];
                          }; -
                          - -
                        • -
                        -

                        And finally, the Health export an auxiliary method overallStatus that determine the Status of the component based on the Checks object.

                        -
                        function overallStatus(checks: Checks): Status 
                        -
                        - -

                        The App interface is a set of types and interfaces that standardize the way in which the state of the application is reported. The App API define 3 different types of components from the observability point of view:

                        -
                          -
                        • -

                          Component: a component is any part of the system that has a own identity and can be monitored for error handling. The only requirement is to emit an error event when something goes wrong, to have a name and unique component identifier.

                          -
                          /** Component */
                          export interface Component extends EventEmitter {
                          /** Emitted when the component throw an error*/
                          on(event: 'error', listener: (error: Crash | Error) => void): this;
                          /** Component name */
                          name: string;
                          /** Component identifier */
                          componentId: string;
                          } -
                          - -

                          This interface define:

                          -
                            -
                          • Properties: -
                              -
                            • name: the name of the component, this name is used by the observability layers to identify the component.
                            • -
                            • componentId: a unique identifier for the instance in UUID v4 format.
                            • -
                            -
                          • -
                          • Events: -
                              -
                            • on('error', listener: (error: Crash | Error) => void): this: event emitted every time the Component emits an error.
                            • -
                            -
                          • -
                          -
                        • -
                        • -

                          Resource: a resource is extended component that represent the access to an external/internal resource, besides the error handling and identity, it has a start, stop and close methods to manage the resource lifecycle. It also has a checks property to define the checks that will be performed over the resource to achieve the resulted status. The most typical example of a resource are the Provider that allow to access to external databases, message brokers, etc.

                          -
                          /** Resource */
                          export interface Resource extends Component {
                          /** Emitted when the component throw an error*/
                          on(event: 'error', listener: (error: Crash | Error) => void): this;
                          /** Emitted on every status change */
                          on(event: 'status', listener: (status: Status) => void): this;
                          /** Checks performed over this component to achieve the resulted status */
                          checks: Checks;
                          /** Resource status */
                          status: Status;
                          /** Resource start function */
                          start: () => Promise<void>;
                          /** Resource stop function */
                          stop: () => Promise<void>;
                          /** Resource close function */
                          close: () => Promise<void>;
                          } -
                          - -

                          Besides the Component properties and events, this interface define:

                          -
                            -
                          • Properties: -
                              -
                            • checks: list of checks performed by the component to determine its state. It is a list of objects of type Health.Checks.
                            • -
                            • status: the current status of the Resource. It is a variable of type Health.Status whose value can be: -
                                -
                              • pass: indicates that the Resource is in a normal operating state. If all the checks are in pass state, the Resource will be in pass state.
                              • -
                              • fail: indicates that the Resource is in an error state. If any of the checks are in fail state, the Resource will be in fail state.
                              • -
                              • warn: indicates that the Resource is in a warning state. If any of the checks are in warn state, the Resource will be in warn state.
                              • -
                              -
                            • -
                            -
                          • -
                          • Methods: -
                              -
                            • start(): Promise<void>: initialize the Resource, internal jobs, external dependencies connections ....
                            • -
                            • stop(): Promise<void>: stops the Resource, close connections, stop internal jobs, etc.
                            • -
                            • close(): Promise<void>: closes the Resource, release resources, destroy connections, etc.
                            • -
                            -
                          • -
                          • Events: -
                              -
                            • on('status', listener: (status: Health.Status) => void): this: event emitted every time the Resource changes its state.
                            • -
                            -
                          • -
                          -
                        • -
                        • -

                          Service: a service is a special kind of resource that besides Resource properties, it could offer:

                          -
                            -
                          • Its own REST API endpoints, using an express router, to expose details about service.
                          • -
                          • A links property to define the endpoints that the service expose.
                          • -
                          • A metrics property to expose the metrics registry where the service will register its own metrics. This registry should be a prom-client registry.
                          • -
                          -
                          /** Service */
                          export interface Service extends Resource {
                          /** Express router */
                          router?: Router;
                          /** Service base path */
                          links?: Links;
                          /** Metrics registry */
                          metrics?: Registry;
                          } -
                          - -

                          Besides the Resource properties, methods and events, this interface define:

                          -
                            -
                          • Properties: -
                              -
                            • router: an express router that will be used to expose the service endpoints.
                            • -
                            • links: an object containing link relations and URIs [RFC3986] for external links that may contain more information about the health of the endpoint. This includes potentially a “self” link, which may be used by clients to check health via HTTP response code.
                            • -
                            • metrics: a metrics registry that will be used to register the service metrics. This registry should be a prom-client registry.
                            • -
                            -
                          • -
                          -
                        • -
                        -

                        The Provider API of @mdf.js allows for the instrumentation of resource providers (databases, publish/subscribe services, etc.) so they can be managed in a standardized way within the @mdf.js API, especially in terms of:

                        -
                          -
                        • Observability, as all Providers implement the Layer.App.Resource interface.
                        • -
                        • Configuration management, providing an interface for managing default, specific, or environment variable-based configurations.
                        • -
                        • Resource provider state management, through the standardization of the states and operation modes of the Providers.
                        • -
                        -

                        Some examples of providers instrumented with this API are:

                        - -

                        A provider that has been correctly instrumented with @mdf.js/core API always offers a Factory class with a single static method create that allows creating new instances of the provider with the desired configuration for each case.

                        -

                        This create method may receive a configuration object with the following optional properties:

                        -
                          -
                        • name: the name of the provider that will be used for observability, if not specified, the default provider name will be used.
                        • -
                        • logger: a LoggerInstance object, belonging to the @mdf.js/logger module or any other object that implements the LoggerInstance interface. If specified, it will be used by both the Provider and the Port it wraps. If not specified, a DEBUG type logger will be used with the provider's name indicated in the name property, or if not specified, with the default provider name.
                        • -
                        • config: specific configuration object for the module wrapped by the Provider in question. If not specified, the default configuration set by the Provider developer will be used.
                        • -
                        • useEnvironment: this property can be a boolean or a string, with its default value being false. It can take the following values: -
                            -
                          • boolean: -
                              -
                            • true: indicates that the environment variables defined by the Provider developer should be used, combined with the Provider's default values and the configuration passed as an argument. The configuration is established in this order of priority: first, the arguments provided directly are taken into account, then the configurations defined in the system's environment variables, and lastly, if none of the above is available, the default values are applied.
                            • -
                            • false: indicates that the environment variables defined by the Provider developer should NOT be used, only the default values will be combined with the configuration passed as an argument. In this case, the configuration is established in this order of priority: first, the arguments provided directly are taken into account, and then the default values.
                            • -
                            -
                          • -
                          • string: if a string is passed, it will be used as a prefix for the environment configuration variables, represented in SCREAMING_SNAKE_CASE, which will be transformed to camelCase and combined with the rest of the configuration, except with the environment variables defined by the Provider developer. In this case, the configuration is established in this order of priority: first, the arguments provided directly are taken into account, then the configurations defined in the system's environment variables, and lastly, if none of the above is available, the default values are applied.
                          • -
                          -
                        • -
                        -
                        -

                        Note: The aim of this configuration handling is to allow the user to work in two different modes:

                        -
                          -
                        • User rules: the user sets their own configuration, disregarding the environment variables indicated by the Provider developer, with the alternative of being able to use a fast track of environment variables usage through a prefix. That is: useEnvironment: false or useEnvironment: 'MY_PREFIX_'.
                        • -
                        • Provider rules: the user prefers to use the environment variables defined by the Provider developer, in which case the management of the environment variables should be delegated to the Provider, allowing the user to set specific configuration values through the input argument, an attempt to create a mixed configuration where both the Provider and the service/application try to use environment variables, can lead to undesirable situations. That is: useEnvironment: true.
                        • -
                        -
                        -
                        import { Mongo } from '@mdf.js/mongo-provider';
                        // Using only `Provider` default values:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create();
                        // Using `Provider` default values and custom values:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [x] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        });
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: false
                        });
                        // Using `Provider` default values, custom values and `Provider` environment variables:
                        // - [x] `Provider` default values
                        // - [x] `Provider` environment variables
                        // - [x] User custom values
                        // - [] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: true
                        });
                        // Using `Provider` default values, custom values and `Provider` environment variables with a prefix:
                        // - [x] `Provider` default values
                        // - [] `Provider` environment variables
                        // - [x] User custom values
                        // - [x] Parsing of environment variables
                        const myProvider = Mongo.Factory.create({
                        config: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        useEnvironment: 'MY_PREFIX_'
                        }); -
                        - -

                        Now that we have our provider instance, let's see what it offers:

                        -
                          -
                        • Properties: -
                            -
                          • -

                            componentId: a unique identifier for the instance in UUID v4 format.

                            -
                          • -
                          • -

                            name: the name of the provider, by default, set by the Provider developer or the name provided in the configuration.

                            -
                          • -
                          • -

                            config: the resulting configuration that was used to create the instance.

                            -
                          • -
                          • -

                            state: the current state of the Provider. It is a variable of type ProviderState whose value can be:

                            -
                              -
                            • running: indicates that the Provider is in a normal operating state.
                            • -
                            • stopped: indicates that the Provider has been stopped or has not been initialized.
                            • -
                            • error: indicates that the Provider has encountered an error in its operation.
                            • -
                            -
                          • -
                          • -

                            error: in case the Provider is in an error state, this property will contain an object with the error information. This property is of type ProviderError, which is a type alias for Crash | Multi | undefined, you can find more information about Crash and Multi types in the documentation of @mdf.js/crash.

                            -
                          • -
                          • -

                            date: the date in ISO 8601 format of the last update of the Provider's state.

                            -
                          • -
                          • -

                            checks: list of checks performed by the Provider to determine its state. It is a list of objects of type Health.Checks, which will contain at least one entry with the information of the state check of the Provider under the property [${name}:status] where ${name} is the name of the Provider, indicated in the configuration or by default. This field will contain an array with a single object of type Health.Check that will contain the information of the state check of the Provider. Example value of checks:

                            -
                            {
                            "myName:status": [
                            {
                            "status": "pass", // "pass" | "fail" | "warn"
                            "componentId": "00000000-0000-0000-0000-000000000000", // UUID v4
                            "componentType": "connection", // or any other type indicated by the `Provider` developer
                            "observedValue": "running", // "running" | "stopped" | "error"
                            "time": "2024-10-10T10:10:10.000Z",
                            "output": undefined, // or the information of the error property
                            }
                            ]
                            } -
                            - -

                            The Provider developer can add more checks to the list of checks to provide more information about the state of the Provider or the resources it manages.

                            -
                          • -
                          • -

                            client: instance of the client/resource wrapped by the Provider that has been created with the provided configuration.

                            -
                          • -
                          -
                        • -
                        -
                        -

                        Note: the instance returned by the create method is an instance of Provider with generic types for the config (PortConfig) and client (PortClient) properties, which should be extended by the Provider developer to provide a better usage experience, so that the user can know both the configuration and the client that is being used, as well as the client that has been wrapped.

                        -
                        -
                          -
                        • Methods: -
                            -
                          • async start(): Promise<void>: starts the Provider and the resource it wraps.
                          • -
                          • async stop(): Promise<void>: stops the Provider and the resource it wraps.
                          • -
                          • async fail(error: Crash | Error): Promise<void>: sets the Provider in an error state and saves the error information.
                          • -
                          -
                        • -
                        • Events: -
                            -
                          • on('error', listener: (error: Crash | Error) => void): this: event emitted every time the Provider emits an error.
                          • -
                          • on('status', listener: (status: Health.Status) => void): this: event emitted every time the Provider changes its state.
                          • -
                          -
                        • -
                        -

                        To instrument a provider with the @mdf.js/core Provider API, the following actions must be taken:

                        -
                          -
                        • Use the abstract class Port, provided by the API, which must be extended by the Provider developer.
                        • -
                        • Define the properties of the PortConfigValidationStruct object, which indicate the default values of the Provider, the values coming from environment variables, and the validation object of type 'Schema' from the Joi module.
                        • -
                        • Use the ProviderFactoryCreator function to create an instance of the Provider, the class of type Mixin that standardizes the creation of Provider instances with the desired configuration.
                        • -
                        -

                        Let's see point by point how the instrumentation of a Provider is carried out.

                        -

                        The Port should be extended to implement a new specific Port. This class implements some util logic to facilitate the creation of new Ports, for this reason is exposed as abstract class, instead of an interface. The developer should keep the constructor signature, in order to maintain the compatibility with the ProviderFactoryCreator function.

                        -

                        class diagram

                        -

                        The basic operations that already implemented in the class are:

                        -
                          -
                        • Properties: -
                            -
                          • uuid: create by the Port class, it is a unique identifier for the port instance, this uuid is used in error traceability.
                          • -
                          • name: the name of the port, by default, set by the Provider developer or the name provided in the configuration.
                          • -
                          • config: the resulting configuration that was used to create the Port instance.
                          • -
                          • logger: a LoggerInstance object, belonging to the @mdf.js/logger module or any other object that implements the LoggerInstance interface. This property is used to log information about the port and the resources it manages. The Port class set the context of the logger to the port name and the uuid, so it's not necessary to include the context and the uuid of the port in the log messages.
                          • -
                          • checks: list of checks performed by the Port by the use of addCheck method, these checks are collected by the Provider, together with the own check of status, and offered to the observability layers.
                          • -
                          -
                        • -
                        -
                        -

                        Note: As the signature of the Port constructor should maintained:

                        -
                        constructor(config: PortConfig, logger: LoggerInstance, name: string)
                        -
                        - -

                        Your Port class extension will receive the config, logger and name properties, and you should call the super constructor with these properties.

                        -
                        -

                        What the developers of the Provider should develop in their own Port class extension is:

                        -
                          -
                        • async start(): Promise<void> method, which is responsible initialize or stablish the connection to the resources.
                        • -
                        • async stop(): Promise<void> method, which is responsible stop services or disconnect from the resources.
                        • -
                        • async close(): Promise<void> method, which is responsible to destroy the services, resources or perform a simple disconnection.
                        • -
                        • state property, a boolean value that indicates if the port is connected (true) or healthy (true) or not (false).
                        • -
                        • client property, that return the PortClient instance that is used to interact with the resources.
                        • -
                        -

                        In the next example you can see the expected behavior of a Port class extension when the start, stop and close methods are called depending on the state of the port:¡.

                        -

                        class diagram

                        -

                        In the other hand, this class extends the EventEmitter class, so it's possible to emit events to notify the status of the port:

                        -
                          -
                        • on('error', listener: (error: Crash) => void): this: should be emitted to notify errors in the resource management or access, this will not change the provider state, but the error will be registered in the observability layers.
                        • -
                        • on('closed', listener: (error?: Crash) => void): this: should be emitted if the access to the resources is not longer possible. This event should not be emitted when stop or close methods are used. If the event includes an error, the provider will indicate this error as the cause of the port closure and will be registered in the observability layers.
                        • -
                        • on('unhealthy', listener: (error: Crash) => void): this: should be emitted when the port has limited access or no access to the resources, but the provider is still running and trying to recover the access. If the event includes an error, the provider will indicate this error as the cause of the port unhealthiness and will be registered in the observability layers.
                        • -
                        • on('healthy', listener: () => void): this: should be emitted when the port has recovered the access to the resources.
                        • -
                        -

                        class diagram

                        -

                        Check some examples of implementation in:

                        - -

                        The PortConfigValidationStruct object is a type that defines the default values of the Provider, the values coming from environment variables, and the validation object of type 'Schema' from the Joi module.

                        -

                        The PortConfigValidationStruct object should have the following properties:

                        -
                          -
                        • defaultConfig: an object with the default values of the Provider.
                        • -
                        • envBaseConfig: an object with the environment variables that the Provider will use, if any.
                        • -
                        • schema: a Joi schema object that will be used to validate the configuration object passed to the Provider.
                        • -
                        -
                        import Joi from 'joi';

                        export const PortConfigValidationStruct = {
                        defaultConfig: {
                        url: 'mongodb://localhost:27017',
                        appName: 'myName',
                        },
                        envBaseConfig: {
                        url: process.env['MONGO_URL'],
                        appName: process.env['MONGO_APP_NAME'],
                        },
                        schema: Joi.object({
                        url: Joi.string().uri().required(),
                        appName: Joi.string().required(),
                        }),
                        }; -
                        - -

                        The ProviderFactoryCreator function is a utility function that allows creating instances of the Provider with the desired configuration. This function receives the following arguments:

                        -
                          -
                        • port: the class that extends the Port class and implements the specific Provider.
                        • -
                        • validation: the PortConfigValidationStruct object that defines the default values, environment variables, and the validation schema of the Provider.
                        • -
                        • defaultName: the default name of the Provider, so that it will be used if the name is not provided in the configuration.
                        • -
                        • type: the type of the Provider, which will be used to identify the kind of Provider in the observability layers.
                        • -
                        -
                        const Factory = ProviderFactoryCreator(MongoPort, myConfig, 'Mongo', 'database');
                        -
                        - -

                        The Factory object returned by the ProviderFactoryCreator function has a single static method create that allows creating new instances of the Provider with the desired configuration for each case.

                        -
                        import { Layer } from '@mdf.js/core';
                        import { LoggerInstance } from '@mdf.js/logger';
                        import { CONFIG_PROVIDER_BASE_NAME } from '../config';
                        import { Client, Config } from './types';

                        export type Client = Console;
                        export type Config = {}

                        export class Port extends Layer.Provider.Port<Client, Config> {
                        /** Client handler */
                        private readonly instance: Client;
                        /** */
                        private interval: NodeJS.Timeout;
                        /**
                        * Implementation of functionalities of an HTTP client port instance.
                        * @param config - Port configuration options
                        * @param logger - Port logger, to be used internally
                        * @param name - Port name, to be used in the logger
                        */
                        constructor(config: Config, logger: LoggerInstance, name: string) {
                        super(config, logger, name);
                        this.instance = console;
                        this.interval = setInterval(this.myCheckFunction, 1000);
                        }
                        /** Stupid check function */
                        private readonly myCheckFunction = (): void => {
                        // Check the client status
                        this.addCheck('myCheck', {
                        status: 'pass',
                        componentId: this.uuid,
                        componentType: 'console',
                        observedValue: 'im stupid',
                        time: new Date().toISOString(),
                        });
                        // Emit the status event
                        this.emit('healthy');
                        }
                        /** Return the underlying port instance */
                        public get client(): Client {
                        return this.instance;
                        }
                        /** Return the port state as a boolean value, true if the port is available, false in otherwise */
                        public get state(): boolean {
                        return true;
                        }
                        /** Initialize the port instance */
                        public async start(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        /** Stop the port instance */
                        public async stop(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        /** Close the port instance */
                        public async close(): Promise<void> {
                        // Nothing to do is a stupid port
                        }
                        } -
                        - -
                        import { Layer } from '@mdf.js/core';
                        import { Config } from './port';
                        import Joi from 'joi';

                        export const config: Layer.Provider.PortConfigValidationStruct<Config> = {
                        defaultConfig: {},
                        envBaseConfig: {},
                        schema: Joi.object({}),
                        }; -
                        - -
                        import { Layer } from '@mdf.js/core';
                        import { configEntry } from '../config';
                        import { Port, Client, Config } from './port';

                        export const Factory = Layer.Provider.ProviderFactoryCreator<Client, Config, Port>(
                        Port,
                        configEntry,
                        `myConsole`,
                        'console'
                        ); -
                        - -
                        import { Factory } from './factory';

                        const myProvider = Factory.create();
                        myProvider.instance.log('Hello world!');
                        console.log(myProvider.state); // true
                        console.log(myProvider.checks); // { "myConsole:status": [{ status: 'pass', ... }], { "myConsole:myCheck": [{ status: 'pass', ... }] }
                        myProvider.on('healthy', () => {
                        console.log('Im healthy');
                        }); -
                        - -

                        The Jobs API from @mdf.js allows for the management of job requests and executions within an @mdf.js application in a simplified and standardized way. The two main elements of this API are:

                        -
                          -
                        • The JobHandler class, which is responsible for "transporting" the information of the jobs to be executed, as well as notifying the execution thereof through events to interested observers.
                        • -
                        • The JobRequest interface defines the structure of job requests.
                        • -
                        -
                        class JobHandler<
                        Type extends string = string,
                        Data = unknown,
                        CustomHeaders extends Record<string, any> = NoMoreHeaders,
                        CustomOptions extends Record<string, any> = NoMoreOptions,
                        >;
                        interface JobRequest<
                        Type extends string = string,
                        Data = unknown,
                        CustomHeaders extends Record<string, any> = NoMoreHeaders,
                        CustomOptions extends Record<string, any> = NoMoreOptions,
                        >; -
                        - -

                        Both the class, JobHandler, and the interface, JobRequest use generic types to define the structure of the data transported in the jobs, as well as to define the custom headers and options that can be added to the jobs. In this way, the Jobs API is flexible and can be used in different contexts and with different types of data. The generic parameters for the JobHandler class and the JobRequest interface are as follows:

                        -
                          -
                        • Type: a string type representing the type or types of job to be executed. This string type can be used to filter the jobs to be executed, so that only jobs of a specific type are executed, or to apply different execution logic depending on the job type. For example, it can be used to execute notification jobs: email, sms, push, etc, so the generic type Type would be declared as type Type = 'email' | 'sms' | 'push'.
                        • -
                        • Data: a generic type representing the structure of the data transported in the jobs. This type can be any type of data, from a primitive type like a number or a string, to a complex object with multiple properties. For example, if you want to send an email, the data could be an object with the properties to, subject, and body.
                        • -
                        • CustomHeaders: a generic type representing the custom headers that can be added to the jobs. This type must be a key-value map, where the key is a string and the value can be any type of data. These custom headers can be used to add additional information to the jobs, such as metadata, authentication information, etc. Custom headers are optional and it is not necessary to add them to the jobs if not needed. By default, the generic type CustomHeaders is NoMoreHeaders, which is a type that does not allow adding custom headers to the jobs. An example of a custom header could be an authentication header containing an access token to an external API: { Authorization: 'Bearer <access token>' }.
                        • -
                        • CustomOptions: a generic type representing the custom options that can be added to the jobs. This type must be a key-value map, where the key is a string and the value can be any type of data. These custom options can be used to add additional information to the jobs, such as specific configurations, execution parameters, etc. Custom options are optional and it is not necessary to add them to the jobs if not needed. By default, the generic type CustomOptions is NoMoreOptions, which is a type that does not allow adding custom options to the jobs. In addition to the custom options, there is the property numberOfHandlers, read the section on the JobHandler class for more information.
                        • -
                        -

                        An example of customizing the generic types of the JobHandler class and the JobRequest interface would be as follows:

                        -
                        import { Jobs } from '@mdf.js/core';
                        type Type = 'email' | 'sms' | 'push';
                        type Data = { to: string; subject: string; body: string };
                        type CustomHeaders = { Authorization: string };
                        type CustomOptions = { retry: number };

                        export type MyOwnJobRequest = Jobs.JobRequest<Type, Data, CustomHeaders, CustomOptions>;
                        export class MyOwnJobHandler extends Jobs.JobHandler<Type, Data, CustomHeaders, CustomOptions> {}

                        const myHandler = new MyOwnJobHandler('multi', { to: '', body: '', subject: '' }, 'email', {
                        headers: { Authorization: '' },
                        retry: 0,
                        });

                        const myHandler2 = new MyOwnJobHandler({
                        data: { to: '', body: '', subject: '' },
                        type: 'email',
                        jobUserId: '123',
                        options: { headers: { Authorization: '' }, retry: 0 },
                        }); -
                        - -

                        Let's look in more detail at the structure of the JobHandler class:

                        -
                          -
                        • -

                          constructor: there are two ways to instantiate a JobHandler:

                          -
                            -
                          • constructor(jobRequest: JobRequest<Type, Data, CustomHeaders>): by using a JobRequest object that contains the information of the job to be executed.
                          • -
                          • constructor(jobUserId: string, data: Data, type?: Type, options?: Options<CustomHeaders>): by using the necessary parameters to create a JobRequest object. -Ultimately, both cases are equivalent, as the JobRequest object contains the same data as the constructor parameters. Thus, we can analyze the parameters for creation through the JobRequest object: -
                              -
                            • type: a string type representing the type of job to be executed. The type of this variable is of the generic type Type, read the section on customization of generic types for more information.
                            • -
                            • data: a generic type representing the structure of the data transported in the jobs. The type of this variable is of the generic type Data, read the section on customization of generic types for more information.
                            • -
                            • options: this parameter is optional and is used to add custom headers or options to the jobs. The type of this variable is an object containing two properties: -
                                -
                              • headers: an object containing the custom headers that will be added to the job. By default, this property is of the type CustomHeaders, read the section on customization of generic types for more information.
                              • -
                              • numberOfHandlers: an integer number indicating the number of handlers that will be used to execute the job. By default, this property is 1, which means that the job has to be confirmed, through the use of the done method, only once. If a value greater than 1 is set, the job has to be confirmed n times, where n is the value of numberOfHandlers.
                              • -
                              -
                            • -
                            • jobUserId: an identifier for the job. It should be used to identify the job in the user's logic. When identifying a job, keep in mind the following: -
                                -
                              • Property uuid: each new instance of JobHandler has a unique identifier that can be accessed only in read mode through the property uuid.
                              • -
                              • Property type: indicates the type of job to be executed.
                              • -
                              • Property jobUserId: identifier of the job that should be used to identify the job in the user's logic. -That is, we can have several jobs whose jobUserId is alarmNotification, being able to have each one of them a different type or not and being all instances uniquely identifiable by their uuid.
                              • -
                              -
                            • -
                            -
                          • -
                          -
                        • -
                        • -

                          Properties:

                          -
                            -
                          • uuid: a unique identifier for the instance of the JobHandler. This identifier is read-only and is generated automatically when creating a new instance of the JobHandler.
                          • -
                          • type: a string type representing the type of job to be executed. This type is of the generic type Type, read the section on customization of generic types for more information.
                          • -
                          • jobUserId: an identifier for the job. It should be used to identify the job in the user's logic.
                          • -
                          • jobUserUUID: is a UUID v5 hash that is generated from the jobUserId. This hash is read-only and is generated automatically when creating a new instance of the JobHandler.
                          • -
                          • status: an enumerated type of Status that indicates the status of the job. The possible values are: -
                              -
                            • Status.PENDING(pending): indicates that the job is pending execution. It is the initial state of a job.
                            • -
                            • Status.PROCESSING(processing): indicates that the job is being processed.
                            • -
                            • Status.COMPLETED(completed): indicates that the job has been completed successfully.
                            • -
                            • Status.FAILED(failed): indicates that the job has failed.
                            • -
                            -
                          • -
                          • data: a generic type representing the structure of the data transported in the jobs. This type is of the generic type Data. When accessing the property for the first time, i.e., when the status is Status.PENDING, the job changes its status to Status.PROCESSING.
                          • -
                          • options: contains the options indicated in the constructor of the class.
                          • -
                          • createdAt: the creation date of the job as a Date object.
                          • -
                          • hasErrors: a boolean value that indicates if the job contains errors. This value is read-only and is set automatically when an error occurs in the job. These errors are included through the done and addError methods.
                          • -
                          • errors: if there are errors in the job, this property contains a Multi object belonging to the @mdf.js/crash module that contains the information of the errors. This property is read-only and is set automatically when an error occurs in the job. These errors are included through the done and addError methods.
                          • -
                          • processTime: if the job has been completed successfully, this property contains the time it took to process the job in milliseconds, otherwise, the value is -1.
                          • -
                          -
                        • -
                        • -

                          Methods:

                          -
                            -
                          • -

                            public addError(error: Crash | Multi): void: adds an error to the job. This method is used to add errors to the job that have occurred during its execution. The errors added through this method are included in the errors property and the hasErrors property is set to true. The error created is of the type ValidationError.

                            -
                          • -
                          • -

                            public done(error?: Crash): void: finishes the job. This method is used to finish the job and change its status to Status.COMPLETED if no error has occurred, or to Status.FAILED if an error has occurred. If an error is provided, it is added to the errors property and the hasErrors property is set to true. This method will have to be called as many times as numberOfHandlers has been set in the constructor, once the number of calls is reached, the job will emit the done event.

                            -
                          • -
                          • -

                            public result(): Result<Type>: returns a Result object containing the information of the job.

                            -
                            /** Job result interface */
                            export interface Result<Type extends string = string> {
                            /** Unique job processing identification */
                            uuid: string;
                            /** Job type */
                            type: Type;
                            /** Timestamp, in ISO format, of the job creation date */
                            createdAt: string;
                            /** Timestamp, in ISO format, of the job resolve date */
                            resolvedAt: string;
                            /** Number of entities processed with success in this job */
                            quantity: number;
                            /** Flag that indicate that the publication process has some errors */
                            hasErrors: boolean;
                            /** Array of errors */
                            errors?: MultiObject;
                            /** User job request identifier, defined by the user */
                            jobUserId: string;
                            /** Unique user job request identification, based on jobUserId */
                            jobUserUUID: string;
                            /** Job status */
                            status: Status;
                            } -
                            - -
                          • -
                          • -

                            public toObject(): JobObject<Type, Data, CustomHeaders, CustomOptions>: returns a JobObject type object containing the information of the job.

                            -
                            /** Job object interface */
                            export interface JobObject<
                            Type extends string = string,
                            Data = any,
                            CustomHeaders extends Record<string, any> = NoMoreHeaders,
                            CustomOptions extends Record<string, any> = NoMoreOptions,
                            > extends JobRequest<Type, Data, CustomHeaders, CustomOptions> {
                            /** Job type identification, used to identify specific job handlers to be applied */
                            type: Type;
                            /** Unique job processing identification */
                            uuid: string;
                            /** Unique user job request identification, generated by UUID V5 standard and based on jobUserId */
                            jobUserUUID: string;
                            ** Job status */
                            status: Status;
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          Events:

                          -
                            -
                          • on(event: 'done', listener: (uuid: string, result: Result<Type>, error?: Multi) => void): this: event emitted when the job has been completed successfully or has failed. The event returns the unique identifier of the job, the information of the job in a Result object type, and an error in case a failure has occurred.
                          • -
                          -
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_crash.html b/docs/modules/_mdf_js_crash.html deleted file mode 100644 index b5af3026..00000000 --- a/docs/modules/_mdf_js_crash.html +++ /dev/null @@ -1,133 +0,0 @@ -@mdf.js/crash | @mdf.js

                        Module @mdf.js/crash

                        @mdf.js/crash

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        @mdf.js/crash

                        -
                        Improved, but simplified, error handling
                        - -
                        - -

                        The goal of @mdf.js/crash is to provide improved, but simplified, error handling, while standardizing error handling across all MMS modules.

                        -
                          -
                        • npm
                        • -
                        -
                        npm install @mdf.js/crash
                        -
                        - -
                          -
                        • yarn
                        • -
                        -
                        yarn add @mdf.js/crash
                        -
                        - -

                        This library provides us with 3 different types of errors to use depending on the context in which we find ourselves:

                        -
                          -
                        • Crash: it is the main type of error, it does not allow adding metadata to the error, which will be specially treated by the logging libraries, as well as relating errors to their causes.
                        • -
                        • Multi: it is the type of error mainly used in validation processes in which we can have more than one error that prevents an information input or a group of parameters from being validated. This type of error is the one returned to us by the @ netin-js / doorkeeper validation libraries.
                        • -
                        • Boom: this type of error standardizes errors in RESTful environments by providing helpers that allow the easy creation of standardized responses to frontend applications.
                        • -
                        -

                        One of the main and common parameters of the three types of error is the message associated with the error. This message indicates the type of error that occurred, always taking into account the good practices:

                        -
                          -
                        • Be clear and unambiguous.
                        • -
                        • Be concise and provide accurate information and only what is necessary.
                        • -
                        • Don't use technical jargon.
                        • -
                        • Be humble, don't blame the user.
                        • -
                        • Avoid using negative words.
                        • -
                        • Indicates the way to fix it to the user.
                        • -
                        • Do not use capital letters.
                        • -
                        • Indicates the correct actions, if any.
                        • -
                        • If there are details about the error, provide them in the corresponding section.
                        • -
                        -

                        According to the standard RFC 4122, it allows us to associate an identifier of operation or request to the error. This is especially useful for tracing errors in requests or transactions that occur between different systems or libraries. The identifier should be created by the process or system that initiates the operation (Frontend, Service ...), being included in all processes and registers (logging with @mdf.js/logger) so that this can be used in troubleshooting processes. identifier as filter, allowing easy extraction.

                        -

                        Logging example

                        -

                        Simple example of using the Crash error type.

                        -
                        import { Crash } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const enhancedError = new Crash('Example', v4());
                        console.log(enhancedError.message); // 'Example' -
                        - -

                        or even simpler

                        -
                        import { Crash } from '@mdf.js/crash'

                        const enhancedError = new Crash('Example');
                        console.log(enhancedError.message); // 'Example' -
                        - -

                        Crash allows us to add extra information about the error that can be used by higher layers of our application or at the time of recording the errors.

                        -
                        import { Crash } from './Crash';
                        import fs from 'fs';
                        import { v4 } from 'uuid';

                        const operationId = v4();
                        try {
                        const myContent = fs.readFileSync('path/to/file');
                        } catch (error) {
                        const enhancedError = new Crash(`Error reading the configuration file`, operationId, {
                        cause: error as Error,
                        name: 'FileError',
                        info: {
                        path: 'path/to/file',
                        },
                        });
                        console.log(enhancedError.trace());
                        // [ 'FileError: Error reading the configuration file',
                        // 'caused by Error: ENOENT: no such file or directory, open \'path/to/file\'' ]
                        } -
                        - -

                        Crash allows us to easily determine if an error has a specific cause, being able to act differently for each cause.

                        -
                        ourPromiseThatRejectCrash()
                        .then(()=>{
                        // Our code in case of success
                        })
                        .catch(error => {
                        if (error.hasCauseWithName('FileError')) {
                        // Our code in case of file error
                        } else {
                        // Our code for the rest type of errors
                        }
                        }) -
                        - -

                        Simple example of using Multi type error.

                        -
                        import { Multi } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const enhancedError = new Multi('Example', v4(), {
                        causes: [new Error('My first check that fail'), new Error('My Second check that fail')]
                        }); -
                        - -

                        Errors can be added later, which can be especially useful in transformation processes where various errors can appear during execution.

                        -
                        import { Multi, Crash } from '@mdf.js/crash'
                        import { v4 } from 'uuid';

                        const arrayOfNumbers: number[] = [];
                        const operationId = v4();
                        let enhancedError: Multi | undefined;
                        for (let idx = 0; idx < 10; idx++) {
                        arrayOfNumbers.push(Math.random() * (10 - 0) + 0);
                        }
                        for (const entry of arrayOfNumbers) {
                        if (entry > 5) {
                        const newError = new Crash(`Number of of range`, operationId, {
                        name: 'ValidationError',
                        info: {
                        number: entry,
                        },
                        });
                        if (enhancedError) {
                        enhancedError.push(newError);
                        } else {
                        enhancedError = new Multi(`Errors during validation process`, operationId, {
                        causes: [newError],
                        });
                        }
                        }
                        }
                        if (enhancedError) {
                        console.log(enhancedError.trace());
                        } -
                        - -

                        The most typical way to use the Boom type of error is through helpers, thanks to them, we can create information-rich errors, within the context of our REST API, in a simple way.

                        -
                        import express from 'express';
                        import { BoomHelpers } from '@mdf.js/crash';
                        import { v4 } from 'uuid';
                        const app = express();
                        const port = 3000;

                        app.get('/', (req, res) => {
                        const enhancedError = BoomHelpers.internalServerError('Error during request processing', v4());
                        res.status(enhancedError.status).json(enhancedError);
                        });

                        app.listen(port, () => {
                        console.log(`Example app listening at http://localhost:${port}`)
                        }); -
                        - -

                        Any request to the previous endpoint will return the following result:

                        -
                        {
                        "uuid": "2a931651-6921-4bda-864e-123b69829cff",
                        "status": 500,
                        "code": "HTTP",
                        "title": "Internal Server Error",
                        "detail": "Error during request processing"
                        } -
                        - -

                        We can even provide more information to the user through the options.

                        -
                        import express from 'express';
                        import { BoomHelpers, Crash } from '@mdf.js/crash';
                        import { v4 } from 'uuid';
                        const app = express();
                        const port = 3000;

                        const mock = (req: express.Request, res: express.Response, next: express.NextFunction): void => {
                        req.body = {};
                        req.body.reqId = v4();
                        req.body.order = 'myOrder';
                        next();
                        };

                        function getOrder(order: string, uuid: string): Promise<void> {
                        return Promise.reject(
                        new Crash(`The requested record is not present in the system`, uuid, {
                        name: 'DataNotPresent',
                        info: { order },
                        })
                        );
                        }

                        app.use(mock);
                        app.get('/order', (req, res) => {
                        getOrder(req.body.order, req.body.reqId)
                        .then(result => {
                        res.status(200).json(result);
                        })
                        .catch(error => {
                        const enhancedError = BoomHelpers.badRequest(
                        'Error getting the requested order',
                        req.body.reqId,
                        {
                        cause: error,
                        source: {
                        pointer: req.path,
                        parameters: { order: req.body.order },
                        },
                        name: error.name,
                        info: {
                        detail: error.message,
                        },
                        links: {
                        help: 'help/link/about/orders',
                        },
                        }
                        );
                        res.status(enhancedError.status).json(enhancedError);
                        });
                        });

                        app.listen(port, () => {
                        console.log(`Example app listening at http://localhost:${port}`);
                        }); -
                        - -

                        Any request to the previous endpoint will return the following result:

                        -
                        {
                        "uuid": "59fe72ec-44dc-4cc3-84ec-46c98df00283",
                        "links": {
                        "help": "help/link/about/orders"
                        },
                        "status": 400,
                        "code": "DataNotPresent",
                        "title": "Bad Request",
                        "detail": "Error getting the requested order",
                        "source": {
                        "pointer": "/order",
                        "parameters": {
                        "order": "myOrder"
                        }
                        },
                        "meta": {
                        "detail": "The requested record is not present in the system"
                        }
                        } -
                        - - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Crash

                        Multi

                        Boom

                        Joi integration

                        Other

                        diff --git a/docs/modules/_mdf_js_doorkeeper.html b/docs/modules/_mdf_js_doorkeeper.html deleted file mode 100644 index b6cc2e4e..00000000 --- a/docs/modules/_mdf_js_doorkeeper.html +++ /dev/null @@ -1,55 +0,0 @@ -@mdf.js/doorkeeper | @mdf.js

                        Module @mdf.js/doorkeeper

                        @mdf.js/doorkeeper

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        @mdf.js/doorkeeper

                        -
                        Improved, but simplified, JSON Schema validation using AJV
                        - -
                        - -

                        The goal of @mdf.js/doorkeeper is to provide a simple and robust solution for validating, registering, and managing JSON schemas in diverse applications. The code is designed to leverage advanced JSON schema validation using AJV (Another JSON Schema Validator), enriched with additional functionalities.

                        -
                          -
                        • npm
                        • -
                        -
                        npm install @mdf.js/doorkeeper
                        -
                        - -
                          -
                        • yarn
                        • -
                        -
                        yarn add @mdf.js/doorkeeper
                        -
                        - -

                        This package is part of the @mdf.js project, a collection of packages for building applications with Node.js and Typescript.

                        -

                        @mdf.js/doorkeeper has been designed to store and manage all the JSON schemas used in an application, allowing to assign a unique identifier to each schema, which it is associated with concrete type or interface, in this way, it is possible to validate the data of the application in a simple and robust way and to obtain the type of the data for Typescript applications.

                        -
                        import { Doorkeeper } from '@mdf.js/doorkeeper';

                        export interface User {
                        name: string;
                        age: number;
                        };

                        export interface Address {
                        street: string;
                        city: string;
                        country: string;
                        };

                        const userSchema = {
                        type: 'object',
                        properties: {
                        name: { type: 'string' },
                        age: { type: 'number' },
                        },
                        required: ['name', 'age'],
                        additionalProperties: false,
                        };

                        const addressSchema = {
                        type: 'object',
                        properties: {
                        street: { type: 'string' },
                        city: { type: 'string' },
                        country: { type: 'string' },
                        },
                        required: ['street', 'city', 'country'],
                        additionalProperties: false,
                        };

                        export interface Schemas {
                        'User': User;
                        'Address': Address;
                        }

                        const checker = new Doorkeeper<Schemas>().register({
                        'User': userSchema,
                        'Address': addressSchema,
                        });

                        const user: User = {
                        name: 'John',
                        age: 30,
                        };

                        const address: Address = {
                        street: 'Main Street',
                        city: 'New York',
                        country: 'USA',
                        };

                        const myNewUser = await checker.validate('User', user); // myNewUser is of type User
                        const myNewAddress = await checker.validate('Address', address); // myNewAddress is of type Address -
                        - -
                          -
                        • Doorkeeper
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Doorkeeper

                        Other

                        diff --git a/docs/modules/_mdf_js_elastic_provider.Elastic.html b/docs/modules/_mdf_js_elastic_provider.Elastic.html deleted file mode 100644 index bf778572..00000000 --- a/docs/modules/_mdf_js_elastic_provider.Elastic.html +++ /dev/null @@ -1,24 +0,0 @@ -Elastic | @mdf.js

                        Elastic Provider. -As the rest of the providers, it is a wrapper around a third party library. The only exported -item is namespace Elastic which contains the provider factory, besides some useful types to -manage the provider.

                        -

                        This means that the provider is not directly available, but it must be created using the factory -function.

                        -
                        import { Elastic } from '@mdf.js/elastic-provider';
                        const provider = Elastic.Factory.create(); -
                        - -

                        create function accepts a configuration object as parameter, which is used to configure the -provider. This configuration object is a Layer.Provider.FactoryOptions -object, which is a generic object that can be extended to add provider specific configuration -options.

                        -

                        In this case, the configuration object is a Config object, which is a type exported by -the provider. This object is a ClientOptions object, which is a type exported by the -third party library.

                        -

                        This means that our FactoryOptions object has the next structure:

                        -
                        
                        -
                        - -

                        Index

                        Classes

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_elastic_provider.html b/docs/modules/_mdf_js_elastic_provider.html deleted file mode 100644 index a2602f65..00000000 --- a/docs/modules/_mdf_js_elastic_provider.html +++ /dev/null @@ -1,100 +0,0 @@ -@mdf.js/elastic-provider | @mdf.js

                        Module @mdf.js/elastic-provider

                        @mdf.js/elastic-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/elastic-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        Elasticsearch provider for @mdf.js based on elasticsearch-js.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/elastic-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/elastic-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        The provider implemented in this module wraps the elasticsearch-js client.

                        -
                        import { Elastic } from '@mdf.js/elastic-provider';

                        const elastic = Elastic.Factory.create({
                        name: `myElasticProvider`,
                        config: {...}, // elasticsearch-js - `ClientOptions`
                        logger: myLoggerInstance,
                        useEnvironment: true,
                        }); -
                        - -
                          -
                        • -

                          Defaults:

                          -
                          {
                          nodes: ['http://localhost:9200'],
                          maxRetries: 5,
                          requestTimeout: 30000,
                          pingTimeout: 3000,
                          resurrectStrategy: 'ping',
                          name: process.env['NODE_APP_INSTANCE'] || 'mdf-elastic',
                          } -
                          - -
                        • -
                        • -

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          -
                          {
                          nodes: process.env['CONFIG_ELASTIC_NODES'] || process.env['CONFIG_ELASTIC_NODE'], // If CONFIG_ELASTIC_NODES is set, CONFIG_ELASTIC_NODE is ignored. CONFIG_ELASTIC_NODES is split by ','
                          maxRetries: process.env['CONFIG_ELASTIC_MAX_RETRIES'],
                          requestTimeout: process.env['CONFIG_ELASTIC_REQUEST_TIMEOUT'],
                          pingTimeout: process.env['CONFIG_ELASTIC_PING_TIMEOUT'],
                          resurrectStrategy: process.env['CONFIG_ELASTIC_RESURRECT_STRATEGY'],
                          tls: {
                          ca: CA, // file loaded from process.env['CONFIG_ELASTIC_CA_PATH']
                          cert: CERT, // file loaded from process.env['CONFIG_ELASTIC_CLIENT_CERT_PATH']
                          key: KEY, // file loaded from process.env['CONFIG_ELASTIC_CLIENT_KEY_PATH']
                          rejectUnauthorized: process.env['CONFIG_ELASTIC_HTTP_SSL_VERIFY'],
                          servername: process.env['CONFIG_ELASTIC_TLS_SERVER_NAME'],
                          },
                          name: process.env['CONFIG_ELASTIC_NAME'],
                          auth, // { username: process.env['CONFIG_ELASTIC_AUTH_USERNAME'], password: process.env['CONFIG_ELASTIC_AUTH_PASSWORD'] } if both are set
                          proxy: process.env['CONFIG_ELASTIC_PROXY'],
                          } -
                          - -
                        • -
                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the status of the Elasticsearch nodes using the cat health API and evaluating the number of nodes in red state. -
                            -
                          • observedValue: Actual state of the consumer/producer provider instance [error, running, stopped].
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          • output: in case of error state (status fail), the error message is shown.
                          • -
                          -
                        • -
                        • nodes: Checks and shows the number of nodes in the cluster using the cat nodes API. -
                            -
                          • observedValue: response from the cat nodes API in JSON format.
                          • -
                          • observedUnit: Nodes Health.
                          • -
                          • status: pass if no nodes are in red state, fail in other case.
                          • -
                          • output: in case of fail state At least one of the nodes in the system is red state.
                          • -
                          -
                        • -
                        -
                        {
                        "[mdf-elastic:status]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": "running",
                        "componentType": "database",
                        "output": undefined,
                        },
                        ],
                        "[mdf-elastic:nodes]": [
                        {
                        "status": "pass",
                        "componentId": "00000000-0000-0000-0000-000000000000",
                        "observedValue": { ... },
                        "observedUnit": "Nodes Health",
                        "output": undefined,
                        },
                        ],
                        } -
                        - -
                          -
                        • CONFIG_ELASTIC_NODE (default: undefined): Node to connect to. If CONFIG_ELASTIC_NODES is set, this is ignored.
                        • -
                        • CONFIG_ELASTIC_NODES (default: ['http://localhost:9200']): List of nodes to connect to. If this is set, CONFIG_ELASTIC_NODE is ignored.
                        • -
                        • CONFIG_ELASTIC_MAX_RETRIES (default: 5): Maximum number of retries before failing the request.
                        • -
                        • CONFIG_ELASTIC_REQUEST_TIMEOUT (default: 30000): Time in milliseconds before the request is considered a timeout.
                        • -
                        • CONFIG_ELASTIC_PING_TIMEOUT (default: 3000): Time in milliseconds before the request is considered a timeout.
                        • -
                        • CONFIG_ELASTIC_PROXY (default: undefined): Proxy to use when connecting to the Elasticsearch cluster.
                        • -
                        • CONFIG_ELASTIC_NAME (default: CONFIG_ARTIFACT_ID): Name of the Elasticsearch client.
                        • -
                        • CONFIG_ELASTIC_HTTP_SSL_VERIFY (default: true): Whether to verify the SSL certificate.
                        • -
                        • CONFIG_ELASTIC_CA_PATH (default: undefined): Path to the CA certificate.
                        • -
                        • CONFIG_ELASTIC_CLIENT_CERT_PATH (default: undefined): Path to the client certificate.
                        • -
                        • CONFIG_ELASTIC_CLIENT_KEY_PATH (default: undefined): Path to the client key.
                        • -
                        • CONFIG_ELASTIC_TLS_SERVER_NAME (default: undefined): Server name for the TLS certificate.
                        • -
                        • CONFIG_ELASTIC_AUTH_USERNAME (default: undefined): Username for the Elasticsearch cluster. If this is set, a password must also be provided.
                        • -
                        • CONFIG_ELASTIC_AUTH_PASSWORD (default: undefined): Password for the Elasticsearch cluster. If this is set, a username must also be provided.
                        • -
                        • NODE_APP_INSTANCE: undefined
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_faker.html b/docs/modules/_mdf_js_faker.html deleted file mode 100644 index a1e56f43..00000000 --- a/docs/modules/_mdf_js_faker.html +++ /dev/null @@ -1,107 +0,0 @@ -@mdf.js/faker | @mdf.js

                        Module @mdf.js/faker

                        @mdf.js/faker

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Faker is a tool to generate fake data for testing purposes based on rosie package with some improvements.

                        -

                        To use @mdf.js/faker you first define a factory. The factory is defined in terms of attributes, sequences, options, callbacks, and can inherit from other factories. Once the factory is defined you use it to build objects.

                        -
                          -
                        • npm
                        • -
                        -
                        npm install @mdf.js/faker
                        -
                        - -
                          -
                        • yarn
                        • -
                        -
                        yarn add @mdf.js/faker
                        -
                        - -

                        There are two phases of use:

                        -
                          -
                        1. Factory definition
                        2. -
                        3. Object building
                        4. -
                        -

                        Factory Definition: Define a factory, specifying attributes, sequences, options, and callbacks:

                        -
                        import { Factory } from '@mdf.js/faker';
                        interface Player {
                        id: number;
                        name: string;
                        position: string;
                        }

                        interface Game {
                        id: number;
                        is_over: boolean;
                        created_at: Date;
                        random_seed: number;
                        players: Player[];
                        }

                        const playerFactory = new Factory<Player>()
                        .sequence('id')
                        .sequence('name', i => {
                        return 'player' + i;
                        })

                        // Define `position` to depend on `id`.
                        .attr('position', ['id'], id => {
                        const positions = ['pitcher', '1st base', '2nd base', '3rd base'];
                        return positions[id % positions.length];
                        });

                        const gameFactory = new Factory<Game>()
                        .sequence('id')
                        .attr('is_over', false)
                        .attr('created_at', () => new Date())
                        .attr('random_seed', () => Math.random())
                        // Default to two players. If players were given, fill in
                        // whatever attributes might be missing.
                        .attr('players', ['players'], players => {
                        if (!players) {
                        players = [{}, {}];
                        }
                        return players.map(data => playerFactory.build(data));
                        });

                        const disabledPlayer = new Factory().extend(playerFactory).attr('state', 'disabled'); -
                        - -

                        Object Building: Build an object, passing in attributes that you want to override:

                        -
                        const game = gameFactory.build({ is_over: true });
                        // Built object (note scores are random):
                        //{
                        // id: 1,
                        // is_over: true, // overriden when building
                        // created_at: Fri Apr 15 2011 12:02:25 GMT-0400 (EDT),
                        // random_seed: 0.8999513240996748,
                        // players: [
                        // {id: 1, name:'Player 1'},
                        // {id: 2, name:'Player 2'}
                        // ]
                        //} -
                        - -

                        You can specify options that are used to programmatically generate the attributes:

                        -
                        import { Factory } from '@mdf.js/faker';
                        import moment from 'moment';

                        interface Match {
                        matchDate: string;
                        homeScore: number;
                        awayScore: number;
                        }

                        const matchFactory = new Factory<Match>()
                        .attr('seasonStart', '2016-01-01')
                        .option('numMatches', 2)
                        .attr('matches', ['numMatches', 'seasonStart'], (numMatches, seasonStart) => {
                        const matches = [];
                        for (const i = 1; i <= numMatches; i++) {
                        matches.push({
                        matchDate: moment(seasonStart).add(i, 'week').format('YYYY-MM-DD'),
                        homeScore: Math.floor(Math.random() * 5),
                        awayScore: Math.floor(Math.random() * 5),
                        });
                        }
                        return matches;
                        });

                        matchFactory.build({ seasonStart: '2016-03-12' }, { numMatches: 3 });
                        // Built object (note scores are random):
                        //{
                        // seasonStart: '2016-03-12',
                        // matches: [
                        // { matchDate: '2016-03-19', homeScore: 3, awayScore: 1 },
                        // { matchDate: '2016-03-26', homeScore: 0, awayScore: 4 },
                        // { matchDate: '2016-04-02', homeScore: 1, awayScore: 0 }
                        // ]
                        //} -
                        - -

                        In the example numMatches is defined as an option, not as an attribute. Therefore numMatches is not part of the output, it is only used to generate the matches array.

                        -

                        In the same example seasonStart is defined as an attribute, therefore it appears in the output, and can also be used in the generator function that creates the matches array.

                        -

                        The convenience function attrs simplifies the common case of specifying multiple attributes in a batch. Rewriting the game example from above:

                        -
                        const gameFactory = new Factory()
                        .sequence('id')
                        .attrs({
                        is_over: false,
                        created_at: () => new Date(),
                        random_seed: () => Math.random(),
                        })
                        .attr('players', ['players'], players => {
                        /* etc. */
                        }); -
                        - -

                        You can also define a callback function to be run after building an object:

                        -
                        interface Coach {
                        id: number;
                        players: Player[];
                        }

                        const coachFactory = new Factory()
                        .option('buildPlayer', false)
                        .sequence('id')
                        .attr('players', ['id', 'buildPlayer'], (id, buildPlayer) => {
                        if (buildPlayer) {
                        return [Factory.build('player', { coach_id: id })];
                        }
                        })
                        .after((coach, options) => {
                        if (options.buildPlayer) {
                        console.log('built player:', coach.players[0]);
                        }
                        });

                        Factory.build({}, { buildPlayer: true }); -
                        - -

                        Multiple callbacks can be registered, and they will be executed in the order they are registered. The callbacks can manipulate the built object before it is returned to the callee.

                        -

                        If the callback doesn't return anything, @mdf.js/faker will return build object as final result. If the callback returns a value, @mdf.js/faker will use that as final result instead.

                        -

                        This is an advanced use case that you can probably happily ignore, but store this away in case you need it.

                        -

                        When you define a factory you can optionally provide a class definition, and anything built by the factory will be passed through the constructor of the provided class.

                        -

                        Specifically, the output of .build is used as the input to the constructor function, so the returned object is an instance of the specified class:

                        -
                        class SimpleClass {
                        constructor(args) {
                        this.moops = 'correct';
                        this.args = args;
                        }

                        isMoopsCorrect() {
                        return this.moops;
                        }
                        }

                        testFactory = Factory.define('test', SimpleClass).attr('some_var', 4);

                        testInstance = testFactory.build({ stuff: 2 });
                        console.log(JSON.stringify(testInstance, {}, 2));
                        // Output:
                        // {
                        // "moops": "correct",
                        // "args": {
                        // "stuff": 2,
                        // "some_var": 4
                        // }
                        // }

                        console.log(testInstance.isMoopsCorrect());
                        // Output:
                        // correct -
                        - -

                        Mind. Blown.

                        -

                        To use @mdf.js/faker in node, you'll need to import it first:

                        -
                        import { Factory } from '@mdf.js/faker';
                        // or with `require`
                        const Factory = require('@mdf.js/faker').Factory; -
                        - -

                        You might also choose to use unregistered factories, as it fits better with node's module pattern:

                        -
                        // factories/game.js
                        import { Factory } from '@mdf.js/faker';

                        export default new Factory().sequence('id').attr('is_over', false);
                        // etc -
                        - -

                        To use the unregistered Game factory defined above:

                        -
                        import Game from './factories/game';

                        const game = Game.build({ is_over: true }); -
                        - -

                        You can also extend an existing unregistered factory:

                        -
                        // factories/scored-game.js
                        import { Factory } from '@mdf.js/faker';
                        import Game from './game';

                        export default new Factory().extend(Game).attrs({
                        score: 10,
                        }); -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Classes

                        Interfaces

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_firehose.Plugs.Sink.html b/docs/modules/_mdf_js_firehose.Plugs.Sink.html deleted file mode 100644 index 4e4619c0..00000000 --- a/docs/modules/_mdf_js_firehose.Plugs.Sink.html +++ /dev/null @@ -1,7 +0,0 @@ -Sink | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Jet -

                        Type Aliases

                        Any -Tap -
                        diff --git a/docs/modules/_mdf_js_firehose.Plugs.Source.html b/docs/modules/_mdf_js_firehose.Plugs.Source.html deleted file mode 100644 index 1df4db50..00000000 --- a/docs/modules/_mdf_js_firehose.Plugs.Source.html +++ /dev/null @@ -1,5 +0,0 @@ -Source | @mdf.js

                        Index

                        Interfaces

                        Type Aliases

                        Any -
                        diff --git a/docs/modules/_mdf_js_firehose.Plugs.html b/docs/modules/_mdf_js_firehose.Plugs.html deleted file mode 100644 index a382fa4a..00000000 --- a/docs/modules/_mdf_js_firehose.Plugs.html +++ /dev/null @@ -1,3 +0,0 @@ -Plugs | @mdf.js

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_firehose.html b/docs/modules/_mdf_js_firehose.html deleted file mode 100644 index bb2f28d4..00000000 --- a/docs/modules/_mdf_js_firehose.html +++ /dev/null @@ -1,33 +0,0 @@ -@mdf.js/firehose | @mdf.js

                        Module @mdf.js/firehose

                        @mdf.js

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        Classes

                        Interfaces

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_http_client_provider.HTTP.html b/docs/modules/_mdf_js_http_client_provider.HTTP.html deleted file mode 100644 index bd657776..00000000 --- a/docs/modules/_mdf_js_http_client_provider.HTTP.html +++ /dev/null @@ -1,7 +0,0 @@ -HTTP | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_http_client_provider.html b/docs/modules/_mdf_js_http_client_provider.html deleted file mode 100644 index 5b44ba29..00000000 --- a/docs/modules/_mdf_js_http_client_provider.html +++ /dev/null @@ -1,65 +0,0 @@ -@mdf.js/http-client-provider | @mdf.js

                        Module @mdf.js/http-client-provider

                        @mdf.js/http-client-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/http-client-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        HTTP client provider for @mdf.js based on axios.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/http-client-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/http-client-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Due to the nature of the HTTP client, the status check is not implemented. The provider is always in running state. -
                            -
                          • observedValue: running.
                          • -
                          • status: pass.
                          • -
                          • output: undefined.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_HTTP_CLIENT_BASE_URL (default: undefined): Base URL for the HTTP client requests.
                        • -
                        • CONFIG_HTTP_CLIENT_TIMEOUT (default: undefined): Time in milliseconds before the request is considered a timeout.
                        • -
                        • CONFIG_HTTP_CLIENT_AUTH_USERNAME (default: undefined): Username for the HTTP client authentication, if username is set, password must be set too.
                        • -
                        • CONFIG_HTTP_CLIENT_AUTH_PASSWORD (default: undefined): Password for the HTTP client authentication if password is set, username must be set too.
                        • -
                        • CONFIG_HTTP_CLIENT_KEEPALIVE (default: false): Keep sockets around in a pool to be used by other requests in the future.
                        • -
                        • CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY (default: undefined): Time in milliseconds before the keep alive feature is enabled.
                        • -
                        • CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS (default: 1000): When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. Only relevant if keepAlive is set to true.
                        • -
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS (default: Infinity): Maximum number of sockets to allow per host. Default for Node 0.10 is 5, default for Node 0.12 is Infinity.
                        • -
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL (default: Infinity): Maximum number of sockets allowed for all hosts in total. Each request will use a new socket until the maximum is reached. Default: Infinity.
                        • -
                        • CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE (default: 256): Maximum number of sockets to leave open in a free state. Only relevant if keepAlive is set to true.
                        • -
                        • CONFIG_HTTP_CLIENT_REJECT_UNAUTHORIZED (default: true): Reject unauthorized TLS certificates.
                        • -
                        • CONFIG_HTTP_CLIENT_CA_PATH (default: undefined): Path to the CA certificate file.
                        • -
                        • CONFIG_HTTP_CLIENT_CLIENT_CERT_PATH (default: undefined): Path to the client certificate file.
                        • -
                        • CONFIG_HTTP_CLIENT_CLIENT_KEY_PATH (default: undefined): Path to the client key file.
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_http_server_provider.HTTP.html b/docs/modules/_mdf_js_http_server_provider.HTTP.html deleted file mode 100644 index 3249ab6f..00000000 --- a/docs/modules/_mdf_js_http_server_provider.HTTP.html +++ /dev/null @@ -1,7 +0,0 @@ -HTTP | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_http_server_provider.html b/docs/modules/_mdf_js_http_server_provider.html deleted file mode 100644 index db40feea..00000000 --- a/docs/modules/_mdf_js_http_server_provider.html +++ /dev/null @@ -1,53 +0,0 @@ -@mdf.js/http-server-provider | @mdf.js

                        Module @mdf.js/http-server-provider

                        @mdf.js/http-server-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/http-server-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        HTTP server provider for @mdf.js based on express.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/http-server-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/http-server-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Due to the nature of the HTTP server, the status could be running if the server has been started properly, stopped if the server has been stopped or is not initialized, or error if the server could not be started. -
                            -
                          • observedValue: running if the server is running, stopped if the server is stopped, or error if the server could not be started.
                          • -
                          • status: pass if the server is running, fail could not be started or warn if the server is stopped.
                          • -
                          • output: In case of error state (status fail), the error message is shown.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_SERVER_PORT (default: 8080): Port for the HTTP server.
                        • -
                        • CONFIG_SERVER_HOST (default: localhost): Host for the HTTP server.
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_jsonl_archiver.JSONLArchiver.html b/docs/modules/_mdf_js_jsonl_archiver.JSONLArchiver.html deleted file mode 100644 index 36b2c9bd..00000000 --- a/docs/modules/_mdf_js_jsonl_archiver.JSONLArchiver.html +++ /dev/null @@ -1,5 +0,0 @@ -JSONLArchiver | @mdf.js

                        References

                        Type Aliases

                        Variables

                        References

                        Renames and re-exports ArchiverManager
                        diff --git a/docs/modules/_mdf_js_jsonl_archiver.html b/docs/modules/_mdf_js_jsonl_archiver.html deleted file mode 100644 index 87972927..00000000 --- a/docs/modules/_mdf_js_jsonl_archiver.html +++ /dev/null @@ -1,114 +0,0 @@ -@mdf.js/jsonl-archiver | @mdf.js

                        Module @mdf.js/jsonl-archiver

                        @mdf.js/jsonl-archiver-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/jsonl-archiver-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        @mdf.js/jsonl-archiver-provider is a tool designed for managing the storage of jsonl files in Node.js applications. It allows to append data to multiple files, maintaining the append and rotation processes independent for each file.

                        -

                        Abstract the management of jsonl files in Node.js applications, providing a simple and efficient way to store data in a jsonl format. This module is designed to be used in the @mdf.js environment, but it can be used in any Node.js application. Some examples of use cases are:

                        -
                          -
                        • Store logs in a jsonl format
                        • -
                        • Store data in a jsonl format
                        • -
                        -
                          -
                        • Append data to files (as per jsonl format, each line is a json string), with: -
                            -
                          • Automatic file rotation by size, lines or time, moving the file to an archive folder.
                          • -
                          • Automatic destination file selection by json properties.
                          • -
                          • Automatic skip data to be appended by json properties.
                          • -
                          -
                        • -
                        -

                        Using npm:

                        -
                        npm install @mdf.js/jsonl-archive-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/jsonl-archive-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        This module is developed as a @mdf.js Provider so that it can be used easily in any application, both in the @mdf.js environment and in any other Node.js application.

                        -

                        In order to use this module, your should use the Factory exposed and create an instance using the create method:

                        -
                        import { Factory } from '@mdf.js/jsonl-file-store-provider';

                        const default = Factory.create(); // Create a new instance with default options

                        const custom = Factory.create({
                        config: {...} // Custom options
                        name: 'custom' // Custom name
                        useEnvironment: true // Use environment variables
                        logger: myLoggerInstance // Custom logger
                        }); -
                        - -

                        The configuration options (config) are the following:

                        -
                          -
                        • -

                          Defaults:

                          -
                          export interface ArchiveOptions {
                          separator?: string;
                          propertyData?: string;
                          propertyFileName?: string;
                          propertySkip?: string;
                          propertySkipValue?: string | number | boolean;
                          defaultBaseFilename?: string;
                          workingFolderPath: string;
                          archiveFolderPath: string;
                          createFolders: boolean;
                          inactiveTimeout?: number;
                          fileEncoding: BufferEncoding;
                          rotationInterval?: number;
                          rotationSize?: number;
                          rotationLines?: number;
                          retryOptions?: RetryOptions;
                          logger?: LoggerInstance;
                          } -
                          - -

                          Where each property has the next meaning:

                          -
                            -
                          • separator (default: \n): Separator to use when writing the data to the file.
                          • -
                          • propertyData (default: undefined): If set, this property will be used to store the data in the file, it could be a nested property in the data object expressed as a dot separated string.
                          • -
                          • propertyFileName (default: undefined): If set, this property will be used as the filename, it could be a nested property in the data object expressed as a dot separated string.
                          • -
                          • propertySkip (default: undefined): If set, this property will be used to skip the data, it could be a nested property in the data object expressed as a dot separated string.
                          • -
                          • propertySkipValue (default: undefined): If set, this value will be used to skip the data, it could be a string, number or boolean. If value is not set, but propertySkip is set, a not falsy value will be used to skip the data, this means that any value that is not false, 0 or '' will be used to skip the data.
                          • -
                          • defaultBaseFilename (default: 'file'): Base filename for the files.
                          • -
                          • workingFolderPath (default: './data/working'): Path to the folder where the working files are stored.
                          • -
                          • archiveFolderPath (default: './data/archive'): Path to the folder where the closed files are stored.
                          • -
                          • createFolders (default: true): If true, it will create the folders if they don't exist.
                          • -
                          • inactiveTimeout (default: undefined): Maximum inactivity time in milliseconds before a handler is cleaned up.
                          • -
                          • fileEncoding (default: 'utf-8'): Encoding to use when writing to files.
                          • -
                          • rotationInterval (default: 600000): Interval in milliseconds to rotate the file.
                          • -
                          • rotationSize (default: 10485760): Max size of the file before rotating it.
                          • -
                          • rotationLines (default: 10000): Max number of lines before rotating the file.
                          • -
                          • retryOptions (default: { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 }): Retry options for the file handler operations. Check the @mdf.js/utils module for more information.
                          • -
                          • logger (default: undefined): Logger instance to use. Check the @mdf.js/logger module for more information.
                          • -
                          -
                        • -
                        • -

                          Environment: remember to set the useEnvironment flag to true to use these environment variables.

                          -
                          { 
                          workingFolderPath: process.env['CONFIG_JSONL_ARCHIVER_WORKING_FOLDER_PATH'],
                          archiveFolderPath: process.env['CONFIG_JSONL_ARCHIVER_ARCHIVE_FOLDER_PATH'],
                          fileEncoding: process.env['CONFIG_JSONL_ARCHIVER_FILE_ENCODING'],
                          createFolders: process.env['CONFIG_JSONL_ARCHIVER_CREATE_FOLDERS'], /* boolean */
                          rotationInterval: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_INTERVAL'], /* number */
                          rotationSize: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_SIZE'], /* number */
                          rotationLines: process.env['CONFIG_JSONL_ARCHIVER_ROTATION_LINES'], /* number */
                          } -
                          - -
                        • -
                        -
                          -
                        • CONFIG_JSONL_ARCHIVER_WORKING_FOLDER_PATH (default: './data/working'): Path to the folder where the open files are stored
                        • -
                        • CONFIG_JSONL_ARCHIVER_ARCHIVE_FOLDER_PATH (default: './data/archive'): Path to the folder where the closed files are stored
                        • -
                        • CONFIG_JSONL_ARCHIVER_FILE_ENCODING (default: 'utf-8'): File encoding
                        • -
                        • CONFIG_JSONL_ARCHIVER_CREATE_FOLDERS (default: true): Create folders if they do not exist
                        • -
                        • CONFIG_JSONL_ARCHIVER_ROTATION_INTERVAL (default: 600000): Interval in milliseconds to rotate the file
                        • -
                        • CONFIG_JSONL_ARCHIVER_ROTATION_SIZE (default: 10485760): Max size of the file before rotating it
                        • -
                        • CONFIG_JSONL_ARCHIVER_ROTATION_LINES (default: 10000): Max number of lines before rotating the file
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        Classes

                        Interfaces

                        diff --git a/docs/modules/_mdf_js_kafka_provider.Consumer.html b/docs/modules/_mdf_js_kafka_provider.Consumer.html deleted file mode 100644 index 9a95e7e9..00000000 --- a/docs/modules/_mdf_js_kafka_provider.Consumer.html +++ /dev/null @@ -1,7 +0,0 @@ -Consumer | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_kafka_provider.Producer.html b/docs/modules/_mdf_js_kafka_provider.Producer.html deleted file mode 100644 index 2fabaa66..00000000 --- a/docs/modules/_mdf_js_kafka_provider.Producer.html +++ /dev/null @@ -1,4 +0,0 @@ -Producer | @mdf.js

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_kafka_provider.html b/docs/modules/_mdf_js_kafka_provider.html deleted file mode 100644 index b8a42f7e..00000000 --- a/docs/modules/_mdf_js_kafka_provider.html +++ /dev/null @@ -1,113 +0,0 @@ -@mdf.js/kafka-provider | @mdf.js

                        Module @mdf.js/kafka-provider

                        @mdf.js/kafka-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/kafka-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        Kafka provider for @mdf.js based on kafkajs.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/kafka-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/kafka-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the status of the kafka nodes using the admin client of the KafkaJS library, performing several requests about the status of the nodes and groups. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based on the response, or not, to admin client requests. error if there is errors during the requests, running if the requests are successful, and stopped if the instance has been stopped or not initialized.
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          • output: Shows the error message in case of error state (status fail).
                          • -
                          -
                        • -
                        • topics: Checks the topics available in the Kafka connection -
                            -
                          • observedValue: List of topics available in the Kafka connection.
                          • -
                          • observedUnit: topics.
                          • -
                          • status: pass if the topics are available, fail in other cases.
                          • -
                          • output: No topics available if the topics are not available.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_KAFKA_PRODUCER__METADATA_MAX_AGE (default: 300000): Maximum time in ms that the producer will wait for metadata
                        • -
                        • CONFIG_KAFKA_PRODUCER__ALLOW_AUTO_TOPIC_CREATION (default: true): Allow auto topic creation
                        • -
                        • CONFIG_KAFKA_PRODUCER__TRANSACTION_TIMEOUT (default: 60000): Transaction timeout in ms
                        • -
                        • CONFIG_KAFKA_PRODUCER__IDEMPOTENT (default: false): Idempotent producer
                        • -
                        • CONFIG_KAFKA_PRODUCER__TRANSACTIONAL_ID (default: undefined): Transactional id
                        • -
                        • CONFIG_KAFKA_PRODUCER__MAX_IN_FLIGHT_REQUEST (default: undefined): Maximum number of in-flight requests
                        • -
                        • CONFIG_KAFKA_PRODUCER__RETRY__MAX_RETRY_TIME (default: 300000): Maximum time in ms that the producer will wait for metadata
                        • -
                        • CONFIG_KAFKA_PRODUCER__RETRY__INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • -
                        • CONFIG_KAFKA_PRODUCER__RETRY__FACTOR (default: 0.2): A multiplier to apply to the retry time
                        • -
                        • CONFIG_KAFKA_PRODUCER__RETRY__MULTIPLIER (default: 2): A multiplier to apply to the retry time
                        • -
                        • CONFIG_KAFKA_PRODUCER__RETRY__RETRIES (default: 5): Maximum number of retries per call
                        • -
                        • CONFIG_KAFKA_CONSUMER__GROUP_ID (default: 'hostname()'): Consumer group id
                        • -
                        • CONFIG_KAFKA_CONSUMER__SESSION_TIMEOUT (default: 30000): The timeout used to detect consumer failures when using Kafka's group management facility. The consumer sends periodic heartbeats to indicate its liveness to the broker. If no heartbeats are received by the broker before the expiration of this session timeout, then the broker will remove this consumer from the group and initiate a rebalance.
                        • -
                        • CONFIG_KAFKA_CONSUMER__REBALANCE_TIMEOUT (default: 60000): The maximum time that the coordinator will wait for each member to rejoin when rebalancing the group.
                        • -
                        • CONFIG_KAFKA_CONSUMER__HEARTBEAT_INTERVAL (default: 3000): The expected time between heartbeats to the consumer coordinator when using Kafka's group management facility. Heartbeats are used to ensure that the consumer's session stays active and to facilitate rebalancing when new consumers join or leave the group. The value must be set lower than sessionTimeout, but typically should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances.
                        • -
                        • CONFIG_KAFKA_CONSUMER__METADATA_MAX_AGE (default: 300000): The period of time in milliseconds after which we force a refresh of metadata even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions.
                        • -
                        • CONFIG_KAFKA_CONSUMER__ALLOW_AUTO_TOPIC_CREATION (default: true): Allow automatic topic creation on the broker when subscribing to or assigning non-existing topics.
                        • -
                        • CONFIG_KAFKA_CONSUMER__MAX_BYTES_PER_PARTITION (default: 1048576): The maximum amount of data per-partition the server will return.
                        • -
                        • CONFIG_KAFKA_CONSUMER__MIN_BYTES (default: 1): Minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait until some is available.
                        • -
                        • CONFIG_KAFKA_CONSUMER__MAX_BYTES (default: 10485760): The maximum amount of data the server should return for a fetch request.
                        • -
                        • CONFIG_KAFKA_CONSUMER_MAX_WAIT_TIME_IN_MS (default: 5000): The maximum amount of time the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy minBytes.
                        • -
                        • CONFIG_KAFKA_CONSUMER__RETRY__MAX_RETRY_TIME (default: 30000): Maximum time in milliseconds to wait for a successful retry
                        • -
                        • CONFIG_KAFKA_CONSUMER__RETRY__INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • -
                        • CONFIG_KAFKA_CONSUMER__RETRY__FACTOR (default: 0.2): A multiplier to apply to the retry time
                        • -
                        • CONFIG_KAFKA_CONSUMER__RETRY__MULTIPLIER (default: 2): A multiplier to apply to the retry time
                        • -
                        • CONFIG_KAFKA_CONSUMER__RETRY__RETRIES (default: 5): Maximum number of retries per call
                        • -
                        • CONFIG_KAFKA_CONSUMER__READ_UNCOMMITTED (default: false): Whether to read uncommitted messages
                        • -
                        • CONFIG_KAFKA_CONSUMER__MAX_IN_FLIGHT_REQUEST (default: undefined): Maximum number of in-flight requests
                        • -
                        • CONFIG_KAFKA_CONSUMER__RACK_ID (default: undefined): The consumer will only be assigned partitions from the leader of the partition to which it is assigned.
                        • -
                        • CONFIG_KAFKA_LOG_LEVEL (default: `error`): Define the log level for the kafka provider, possible values are: - error - warn - info - debug - trace
                        • -
                        • CONFIG_KAFKA_CLIENT__CLIENT_ID (default: hostname): Client identifier
                        • -
                        • CONFIG_KAFKA_CLIENT__BROKERS (default: '127.0.0.1:9092'): Kafka brokers
                        • -
                        • CONFIG_KAFKA_CLIENT__BROKERS (default: '127.0.0.1:9092'): Kafka brokers
                        • -
                        • CONFIG_KAFKA_CLIENT__CONNECTION_TIMEOUT (default: 1000): Time in milliseconds to wait for a successful connection
                        • -
                        • CONFIG_KAFKA_CLIENT__AUTHENTICATION_TIMEOUT (default: 1000): Timeout in ms for authentication requests
                        • -
                        • CONFIG_KAFKA_CLIENT__REAUTHENTICATION_THRESHOLD (default: 1000): When periodic reauthentication (connections.max.reauth.ms) is configured on the broker side, reauthenticate when reauthenticationThreshold milliseconds remain of session lifetime.
                        • -
                        • CONFIG_KAFKA_CLIENT__REQUEST_TIMEOUT (default: 30000): Time in milliseconds to wait for a successful request
                        • -
                        • CONFIG_KAFKA_CLIENT__ENFORCE_REQUEST_TIMEOUT (default: true): The request timeout can be disabled by setting this value to false.
                        • -
                        • CONFIG_KAFKA_MAX_RETRY_TIME (default: 30000): Maximum time in milliseconds to wait for a successful retry
                        • -
                        • CONFIG_KAFKA_INITIAL_RETRY_TIME (default: 300): Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
                        • -
                        • CONFIG_KAFKA_RETRY_FACTOR (default: 0.2): Randomization factor
                        • -
                        • CONFIG_KAFKA_RETRY_MULTIPLIER (default: 2): Exponential factor
                        • -
                        • CONFIG_KAFKA_RETRIES (default: 5): Maximum number of retries per call
                        • -
                        • CONFIG_KAFKA_CLIENT_SSL_ENABLED (default: false): Whether to use SSL
                        • -
                        • CONFIG_KAFKA_CLIENT__SSL__REJECT_UNAUTHORIZED (default: true): Whether to verify the SSL certificate.
                        • -
                        • CONFIG_KAFKA_CLIENT__SSL__SERVER_NAME (default: undefined): Server name for the TLS certificate.
                        • -
                        • CONFIG_KAFKA_CLIENT_SSL_CA_PATH (default: undefined): Path to the CA certificate.
                        • -
                        • CONFIG_KAFKA_CLIENT_SSL_CERT_PATH (default: undefined): Path to the client certificate.
                        • -
                        • CONFIG_KAFKA_CLIENT_SSL_KEY_PATH (default: undefined): Path to the client key.
                        • -
                        • CONFIG_KAFKA_CLIENT__SASL_USERNAME (default: undefined): SASL username
                        • -
                        • CONFIG_KAFKA_CLIENT__SASL_PASSWORD (default: undefined): SASL password
                        • -
                        • NODE_APP_INSTANCE: undefined
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_logger.html b/docs/modules/_mdf_js_logger.html deleted file mode 100644 index e008f4e0..00000000 --- a/docs/modules/_mdf_js_logger.html +++ /dev/null @@ -1,37 +0,0 @@ -@mdf.js/logger | @mdf.js

                        Module @mdf.js/logger

                        @mdf.js

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Classes

                        Interfaces

                        Type Aliases

                        Variables

                        Functions

                        diff --git a/docs/modules/_mdf_js_middlewares.html b/docs/modules/_mdf_js_middlewares.html deleted file mode 100644 index e53f161b..00000000 --- a/docs/modules/_mdf_js_middlewares.html +++ /dev/null @@ -1,52 +0,0 @@ -@mdf.js/middlewares | @mdf.js

                        Module @mdf.js/middlewares

                        @mdf.js

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Classes

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_mongo_provider.Mongo.html b/docs/modules/_mdf_js_mongo_provider.Mongo.html deleted file mode 100644 index ea873bf0..00000000 --- a/docs/modules/_mdf_js_mongo_provider.Mongo.html +++ /dev/null @@ -1,8 +0,0 @@ -Mongo | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_mongo_provider.html b/docs/modules/_mdf_js_mongo_provider.html deleted file mode 100644 index eeb519f3..00000000 --- a/docs/modules/_mdf_js_mongo_provider.html +++ /dev/null @@ -1,79 +0,0 @@ -@mdf.js/mongo-provider | @mdf.js

                        Module @mdf.js/mongo-provider

                        @mdf.js/mongo-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        MongoDB Provider for @mdf.js/mongo-provider

                        -
                        Mytra Development Framework - @mdf.js
                        - -
                        - -

                        MongoDB provider for @mdf.js based on mongodb.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/mongo-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/mongo-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the status of the MongoDB nodes using the heartbeat events from the client. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last heartbeat event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last heartbeat event was successful, error if the provider is running and the last heartbeat event was not successful.
                          • -
                          • observedUnit: status.
                          • -
                          • status: fail if the observed value is error, warn if the observed value is stopped, pass in other case.
                          • -
                          -
                        • -
                        • heartbeat: -
                            -
                          • observedValue: failed if the last heartbeat was not successful, heartbeat information if the last heartbeat was successful.
                          • -
                          • observedUnit: heartbeat result.
                          • -
                          • status: fail if the observed value is failed, pass in other case.
                          • -
                          • output: shows the connection identifier and the failure message in case of failed state (status fail). undefined in other case.
                          • -
                          -
                        • -
                        • lastCommand: -
                            -
                          • observedValue: succeeded if the last command executed in the provider was successful, failed if the last command executed in the provider failed.
                          • -
                          • observedUnit: command result.
                          • -
                          • status: pass if the observed value is succeeded, fail if the observed value is failed.
                          • -
                          • output: Shows the command name and the command failure message in case of failed state (status fail). undefined if the observed value is succeeded.
                          • -
                          -
                        • -
                        • lastFailedCommands: -
                            -
                          • observedValue: Shows the last 10 failed commands executed in the provider, each entry shows the date of the command in ISO format, the command name and the failure message.
                          • -
                          • observedUnit: last failed commands.
                          • -
                          • status: pass.
                          • -
                          • output: undefined.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_MONGO_URL (default: `mongodb://127.0.0.1:27017/mdf`): URL for the mongo database
                        • -
                        • CONFIG_MONGO_CA_PATH (default: undefined): Path to the CA for the mongo database
                        • -
                        • CONFIG_MONGO_CERT_PATH (default: undefined): Path to the cert for the mongo database
                        • -
                        • CONFIG_MONGO_KEY_PATH (default: undefined): Path to the key for the mongo database
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_mqtt_provider.MQTT.html b/docs/modules/_mdf_js_mqtt_provider.MQTT.html deleted file mode 100644 index 6cfa0bd5..00000000 --- a/docs/modules/_mdf_js_mqtt_provider.MQTT.html +++ /dev/null @@ -1,7 +0,0 @@ -MQTT | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_mqtt_provider.html b/docs/modules/_mdf_js_mqtt_provider.html deleted file mode 100644 index 0d0046a6..00000000 --- a/docs/modules/_mdf_js_mqtt_provider.html +++ /dev/null @@ -1,69 +0,0 @@ -@mdf.js/mqtt-provider | @mdf.js

                        Module @mdf.js/mqtt-provider

                        @mdf.js/mqtt-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/mqtt-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        MQTT provider for @mdf.js based on mqtt.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/mqtt-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/mqtt-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the ping messages from the server. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last ping event was successful, error if the provider is running and the last ping event was not successful.
                          • -
                          • observedUnit: status.
                          • -
                          • status: fail if the observed value is error, warn if the observed value is stopped, pass in other case.
                          • -
                          -
                        • -
                        • lastError: -
                            -
                          • observedValue: last error message from the provider.
                          • -
                          • observedUnit: Last error.
                          • -
                          • status: pass.
                          • -
                          • output: last error message from the provider.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_MQTT_URL (default: 'mqtt://localhost:1883'): URL of the server
                        • -
                        • CONFIG_MQTT_PROTOCOL (default: 'mqtt'): Protocol to use
                        • -
                        • CONFIG_MQTT_USERNAME (default: undefined): Username
                        • -
                        • CONFIG_MQTT_PASSWORD (default: undefined): Password
                        • -
                        • CONFIG_MQTT_CLIENT_ID (default: 'mqtt-client'): Client ID
                        • -
                        • CONFIG_MQTT_KEEPALIVE (default: 60): Keepalive in seconds
                        • -
                        • CONFIG_MQTT_CLIENT_CA_PATH (default: undefined): CA file path
                        • -
                        • CONFIG_MQTT_CLIENT_CLIENT_CERT_PATH (default: undefined): Client cert file path
                        • -
                        • CONFIG_MQTT_CLIENT_CLIENT_KEY_PATH (default: undefined): Client key file path
                        • -
                        • NODE_APP_INSTANCE: undefined
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_openc2.Adapters.Dummy.html b/docs/modules/_mdf_js_openc2.Adapters.Dummy.html deleted file mode 100644 index a0db4ce0..00000000 --- a/docs/modules/_mdf_js_openc2.Adapters.Dummy.html +++ /dev/null @@ -1,7 +0,0 @@ -Dummy | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Classes

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_openc2.Adapters.Redis.html b/docs/modules/_mdf_js_openc2.Adapters.Redis.html deleted file mode 100644 index 752ae734..00000000 --- a/docs/modules/_mdf_js_openc2.Adapters.Redis.html +++ /dev/null @@ -1,4 +0,0 @@ -Redis | @mdf.js
                        diff --git a/docs/modules/_mdf_js_openc2.Adapters.SocketIO.html b/docs/modules/_mdf_js_openc2.Adapters.SocketIO.html deleted file mode 100644 index 1eb07dcf..00000000 --- a/docs/modules/_mdf_js_openc2.Adapters.SocketIO.html +++ /dev/null @@ -1,4 +0,0 @@ -SocketIO | @mdf.js
                        diff --git a/docs/modules/_mdf_js_openc2.Adapters.html b/docs/modules/_mdf_js_openc2.Adapters.html deleted file mode 100644 index 187784cf..00000000 --- a/docs/modules/_mdf_js_openc2.Adapters.html +++ /dev/null @@ -1,4 +0,0 @@ -Adapters | @mdf.js

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_openc2.Factory.html b/docs/modules/_mdf_js_openc2.Factory.html deleted file mode 100644 index f2a43316..00000000 --- a/docs/modules/_mdf_js_openc2.Factory.html +++ /dev/null @@ -1,4 +0,0 @@ -Factory | @mdf.js
                        diff --git a/docs/modules/_mdf_js_openc2.html b/docs/modules/_mdf_js_openc2.html deleted file mode 100644 index 08dddd20..00000000 --- a/docs/modules/_mdf_js_openc2.html +++ /dev/null @@ -1,32 +0,0 @@ -@mdf.js/openc2 | @mdf.js

                        Module @mdf.js/openc2

                        @mdf.js

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        Classes

                        Interfaces

                        diff --git a/docs/modules/_mdf_js_openc2_core.Control.html b/docs/modules/_mdf_js_openc2_core.Control.html deleted file mode 100644 index bc469c19..00000000 --- a/docs/modules/_mdf_js_openc2_core.Control.html +++ /dev/null @@ -1,29 +0,0 @@ -Control | @mdf.js
                        diff --git a/docs/modules/_mdf_js_openc2_core.html b/docs/modules/_mdf_js_openc2_core.html deleted file mode 100644 index cad01834..00000000 --- a/docs/modules/_mdf_js_openc2_core.html +++ /dev/null @@ -1,45 +0,0 @@ -@mdf.js/openc2-core | @mdf.js

                        Module @mdf.js/openc2-core

                        @mdf.js

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        Classes

                        Interfaces

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_redis_provider.Redis.html b/docs/modules/_mdf_js_redis_provider.Redis.html deleted file mode 100644 index baaa5d21..00000000 --- a/docs/modules/_mdf_js_redis_provider.Redis.html +++ /dev/null @@ -1,7 +0,0 @@ -Redis | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_redis_provider.html b/docs/modules/_mdf_js_redis_provider.html deleted file mode 100644 index 97737119..00000000 --- a/docs/modules/_mdf_js_redis_provider.html +++ /dev/null @@ -1,68 +0,0 @@ -@mdf.js/redis-provider | @mdf.js

                        Module @mdf.js/redis-provider

                        @mdf.js/redis-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/redis-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        Redis provider for @mdf.js based on ioredis.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/redis-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/redis-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Checks the ping messages from the server. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the last ping event was successful, error if the provider is running and the last ping event was not successful.
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          -
                        • -
                        • memory: -
                            -
                          • observedValue: actual memory usage of the provider instance. The values are in expressed in bytes: used memory / max memory.
                          • -
                          • observedUnit: used memory / max memory.
                          • -
                          • status: fail if there is a problem getting the memory usage, or if the memory usage is greater or equal than 100% of the maximum memory, warn if the memory usage is greater than 80% of the maximum memory, pass in other case.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_REDIS_HOST (default: '127.0.0.1'): REDIS connection host
                        • -
                        • CONFIG_REDIS_PORT (default: 6379): REDIS connection port
                        • -
                        • CONFIG_REDIS_DB (default: 0): REDIS connection database
                        • -
                        • CONFIG_REDIS_PASSWORD (default: undefined): REDIS connection password
                        • -
                        • CONFIG_REDIS_RETRY_DELAY_FACTOR (default: 2000): REDIS connection retry delay factor
                        • -
                        • CONFIG_REDIS_RETRY_DELAY_MAX (default: 60000): REDIS connection retry delay max
                        • -
                        • CONFIG_REDIS_KEEPALIVE (default: 10000): REDIS connection keepAlive
                        • -
                        • CONFIG_REDIS_CONNECTION_TIMEOUT (default: 10000): REDIS connection keepAlive
                        • -
                        • CONFIG_REDIS_CHECK_INTERVAL (default: 60000): REDIS status check interval
                        • -
                        • CONFIG_REDIS_DISABLE_CHECKS (default: false): Disable Redis checks
                        • -
                        • NODE_APP_INSTANCE: undefined
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_s3_provider.S3.html b/docs/modules/_mdf_js_s3_provider.S3.html deleted file mode 100644 index 6ba0e030..00000000 --- a/docs/modules/_mdf_js_s3_provider.S3.html +++ /dev/null @@ -1,6 +0,0 @@ -S3 | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_s3_provider.html b/docs/modules/_mdf_js_s3_provider.html deleted file mode 100644 index 301b2cff..00000000 --- a/docs/modules/_mdf_js_s3_provider.html +++ /dev/null @@ -1,50 +0,0 @@ -@mdf.js/s3-provider | @mdf.js

                        Module @mdf.js/s3-provider

                        @mdf.js/s3-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/s3-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        S3 provider for @mdf.js based on aws-sdk/client-s3.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/s3-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/s3-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -
                          -
                        • CONFIG_S3_REGION (default: 'eu-central-1'): S3 AWS region to which send requests
                        • -
                        • CONFIG_S3_ACCESS_KEY_ID (default: 'MY_ACCESS_KEY_ID'): S3 AWS connection access key identifier
                        • -
                        • CONFIG_S3_SECRET_ACCESS_KEY (default: 'MY_SECRET_ACCESS_KEY'): S3 AWS connection secret access key
                        • -
                        • NODE_APP_INSTANCE (default: 'MY_SECRET_ACCESS_KEY'): S3 AWS connection secret access key
                        • -
                        • CONFIG_S3_SERVICE_ID (default: process.env['NODE_APP_INSTANCE'] || CONFIG_ARTIFACT_ID): S3 unique service identifier
                        • -
                        • CONFIG_S3_PROXY_HTTP (default: undefined): HTTP Proxy URI
                        • -
                        • CONFIG_S3_PROXY_HTTPS (default: undefined): HTTPS Proxy URI
                        • -
                        • NODE_APP_INSTANCE: Default S3 unique service identifier
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        S3 -
                        diff --git a/docs/modules/_mdf_js_service_registry.html b/docs/modules/_mdf_js_service_registry.html deleted file mode 100644 index 704980d9..00000000 --- a/docs/modules/_mdf_js_service_registry.html +++ /dev/null @@ -1,345 +0,0 @@ -@mdf.js/service-registry | @mdf.js

                        Module @mdf.js/service-registry

                        @mdf.js/service-registry

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/service-registry

                        -
                        Service register, used for tooling microservices adding observability and control capabilities. -
                        - -
                        - -

                        The @mdf.js/service-register is a core package of the Mytra Development Framework. This module is designed for instrumenting microservices, adding observability and control capabilities, among other features. This allows developers to focus on the business logic development, instead of implementing these capabilities into each microservice.

                        -

                        In summary, the @mdf.js/service-register module provides the following features:

                        -
                          -
                        • Configuration management: Load configurations from files, environment variables, or the package.json file.
                        • -
                        • Logging: Use a logger with different transports, levels, and formats.
                        • -
                        • Metrics: Collect metrics from the application and expose them through an HTTP server in Prometheus format.
                        • -
                        • Health checks: Expose an HTTP server with health checks for the application.
                        • -
                        • Control Interface: Allow to create a built-in OpenC2 consumer for controlling the application.
                        • -
                        -

                        The @mdf.js/service-register module is intended to be loaded at the start of the application, even supporting the use of cluster for creating multiple instances of the application.

                        -

                        With default parameters:

                        -
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry();
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources -
                        - -

                        With custom parameters:

                        -
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry(
                        {
                        configFiles: ['./config/config.json'],
                        useEnvironment: true,
                        loadReadme: true,
                        },
                        {
                        loggerOptions: {
                        console: {
                        enabled: true,
                        level: 'info',
                        }
                        },
                        metadata: {
                        name: 'service-registry',
                        version: '1.0.0',
                        description: 'Service registry, used for tooling microservices with observability and control capabilities.',
                        }
                        ...
                        },
                        {
                        myOwnProperty: `myNeededValue`
                        }
                        );

                        const myProvider = new MyProvider(service.get('myProviderConfig'));
                        const myResource = new MyResource(service.get('myResourceConfig.option1'));
                        const myService = new MyService(service.settings.custom.myOwnProperty);
                        service.logger.info('My custom log message');
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources -
                        - -

                        Using cluster for creating multiple instances:

                        -
                        import { ServiceRegistry } from '@mdf.js/service-registry';
                        import cluster from 'cluster';

                        if (cluster.isMaster) {
                        const service = new ServiceRegistry(
                        {},
                        {
                        observabilityOptions: {
                        isCluster: true, // Necessary to indicate that the service is running in cluster mode
                        },
                        }
                        );
                        for (let i = 0; i < 4; i++) {
                        cluster.fork({
                        NODE_APP_INSTANCE: `MyOwnIdentifier-${i}`,
                        });
                        }
                        await service.start(); // Even with resources registered, they will not be started
                        } else {
                        const service = new ServiceRegistry();
                        // Our business logic goes here
                        service.register([myProvider, myResource, myService]);
                        await service.start(); // This also starts the registered resources
                        } -
                        - -
                        npm install @mdf.js/service-register
                        -
                        - -
                        yarn add @mdf.js/service-register
                        -
                        - -

                        To better understand how this module works, we will divide the documentation into several parts:

                        -
                          -
                        • Parameterization Options: Parameters that can be passed to the ServiceRegistry class constructor.
                        • -
                        • Module's Programmatic Interface: How to access the module's functionalities from the code.
                        • -
                        • Module's REST-API Interface: How to access the module's functionalities through a REST API.
                        • -
                        • Module's Control Interface: How to control the module's functionalities through a control interface.
                        • -
                        -
                        import { ServiceRegistry } from '@mdf.js/service-registry';

                        const service = new ServiceRegistry(
                        {
                        configFiles: ['./config/config.json'],
                        useEnvironment: true,
                        loadReadme: true,
                        },
                        {
                        loggerOptions: {
                        console: {
                        enabled: true,
                        level: 'info',
                        }
                        },
                        metadata: {
                        name: 'service-registry',
                        version: '1.0.0',
                        description: 'Service registry, used for tooling microservices with observability and control capabilities.',
                        }
                        ...
                        },
                        {
                        myOwnProperty: `myNeededValue`
                        }
                        ); -
                        - -
                          -
                        • BootstrapOptions: Service bootstrap options, primarily allowing configuration of how the module @mdf.js/service-registry loads its settings, enabling loading from files, environment variables, or even the project's package.json file.
                        • -
                        • ServiceRegistryOptions: Used as configuration values for the @mdf.js/service-registry module itself, such as the service name, version, description, etc. They override the default values or values loaded from other sources.
                        • -
                        • CustomOptions: Custom options, used as configuration values for the service being developed. These values can be accessed through the settings.custom property of the ServiceRegistry object. These properties override the default values or values loaded from other sources.
                        • -
                        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        PropertyTypeDescriptionDefault value
                        configFilesstring[]List of files with deploying options to be loaded. The entries could be a file path or glob pattern. It supports configurations in JSON, YAML, TOML, and .env file formats. Check @mdf.js/service-setup-provider for more details.[]
                        presetFilesstring[]List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset. Check @mdf.js/service-setup-provider for more details.[]
                        presetstringPreset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used. Check @mdf.js/service-setup-provider for more details.process.env['CONFIG_CUSTOM_PRESET'] process.env['CONFIG_SERVICE_REGISTRY_PRESET'] undefined
                        useEnvironmentbooleanFlag indicating that the environment configuration variables should be used. The configuration loaded by environment variables will be merged with the rest of the configuration, overriding the configuration from files, but not the configuration passed as argument to Service Registry. When option is set some filters are applied to the environment variables to avoid conflicts in the configuration.
                        The filters are:

                        - CONFIG_METADATA_: Application metadata configuration.
                        - CONFIG_OBSERVABILITY_: Observability service configuration.
                        - CONFIG_LOGGER_: Logger configuration.
                        - CONFIG_RETRY_OPTIONS_: Retry options configuration.
                        - CONFIG_ADAPTER_: Consumer adapter configuration.

                        The loader expect environment configuration variables represented in SCREAMING_SNAKE_CASE, that will parsed to camelCase and merged with the rest of the configuration. The consumer adapter configuration is an exception, due to the kind of configuration, it should be provided by configuration parameters.
                        false
                        loadReadmebooleanFlag indicating that the README.md file should be loaded. If this flag is set to true, the module will scale parent directories looking for a README.md file to load, if the file is found, the README content will be exposed in the observability endpoints. If the flag is a string, the string will be used as the file name to look for.false
                        loadPackagebooleanlag indicating that the package.json file should be loaded. If this flag is set to true, the the module will scale parent directories looking for a package.json file to load, if the file is found, the package information will be used to fullfil the metadata field.

                        - package.name will be used as the metadata.name.
                        - package.version will be used as the metadata.version, and the first part of the version will be used as the metadata.release.
                        - package.description will be used as the metadata.description.
                        - package.keywords will be used as the metadata.tags.
                        - package.config.${name}, where name is the name of the configuration, will be used to find the rest of properties with the same name that in the metadata.

                        This information will be merged with the rest of the configuration, overriding the configuration from files, but not the configuration passed as argument to Service Registry.
                        false
                        consumerbooleanFlag indicating if the OpenC2 Consumer command interface should be enabled. The command interface is a set of commands that can be used to interact with the application. The commands are exposed in the observability endpoints and can be used to interact with the service, or, if a consumer adapter is configured, to interact with the service from a central controller.false
                        -
                          -
                        • -

                          metadata (Metadata): Metadata information of the application or microservice. This information is used to identify the application in the logs, metrics, and traces... and is shown in the service observability endpoints.

                          -
                            -
                          • -

                            Properties:

                            -
                              -
                            • name (string): Name of the application or microservice.
                            • -
                            • description (string): Description of the application or microservice.
                            • -
                            • version (string): Version of the application or microservice.
                            • -
                            • release (string): Release of the application or microservice.
                            • -
                            • instanceId (string): Unique identifier of the application or microservice. This value is generated by the application if it is not provided.
                            • -
                            • serviceId (string): Human readable identifier of the application or microservice, should be unique in the system.
                            • -
                            • serviceGroupId (string): Group of the application or microservice to which it belongs.
                            • -
                            • namespace (string): Service namespace, used to identify declare which namespace the service belongs to. It must start with x- as it is a custom namespace and will be used for custom headers, openc2 commands, etc.
                            • -
                            • tags (string[]): Tags of the application or microservice.
                            • -
                            • links: service links to related services or resources. -
                                -
                              • self (string): Link to the service itself or the service observability endpoints.
                              • -
                              • related (string): Link to related services or resources of the service.
                              • -
                              • about (string): Link to the documentation of the service or the service README.
                              • -
                              -
                            • -
                            -
                          • -
                          • -

                            Default value:

                            -
                            {
                            name: 'mdf-app',
                            version: '0.0.0',
                            release: '0',
                            description: undefined,
                            instanceId: '12345678-1234-...', // This value is generated by the application
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          consumerOptions (ConsumerOptions): OpenC2 Consumer configuration options. This configuration is used to setup the OpenC2 consumer, ff this configuration is not provided the consumer will not be started. The consumer is used to receive OpenC2 commands from a central controller.

                          -
                            -
                          • Properties: -
                              -
                            • id (string): Consumer identifier, used to identify the consumer in the system.
                            • -
                            • maxInactivityTime (number): Maximum time of inactivity before the consumer is stopped.
                            • -
                            • registerLimit (number): Maximum number of commands that can be registered at the same time.
                            • -
                            • retryOptions (RetryOptions): Retry options for the consumer.
                            • -
                            • actionTargetPairs (ActionTargetPairs): Action-Target pairs supported by the consumer. All the commands that are not in this list will be rejected, even if they has been included in the resolver map. If the command is only in this list, a command event will be emitted. Check below for more information.
                            • -
                            • profiles (string[]): Profiles supported by the consumer.
                            • -
                            • actuator (string[]): Actuator instance to be used by the consumer.
                            • -
                            • resolver (ResolverMap): Resolver map used by the consumer to resolve the commands. If a namespace is provided, a default resolver map will be included in order to provide a command interface for observability and control requests: -
                                -
                              • query:${namespace}:health: Query the health of the service.
                              • -
                              • query:${namespace}:stats: Query the metrics of the service.
                              • -
                              • query:${namespace}:errors: Query the errors of the service.
                              • -
                              • query:${namespace}:config: Query the configuration of the service.
                              • -
                              • start:${namespace}:resources: Start the resources of the service. (Only available if the service is NOT in cluster mode).
                              • -
                              • stop:${namespace}:resources: Stop the resources of the service. (Only available if the service is NOT in cluster mode).
                              • -
                              • restart:${namespace}:all: Kill the process, the service restart should be done by an external process manager.
                              • -
                              -
                            • -
                            -
                          • -
                          • Default value: undefined
                          • -
                          -
                        • -
                        • -

                          adapterOptions (AdapterOptions): Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, consumer and adapter options must be provided, in other case the consumer will start with a Dummy adapter with no connection to any external service, so only HTTP commands over the observability endpoints will be processed.

                          -
                            -
                          • Properties: -
                              -
                            • type (string): Type of the adapter, could be redis or socketio.
                            • -
                            • config (Redis.Config | SocketIO.Config): Configuration options for the adapter, depending on the type of adapter. Check the documentations of the providers @mdf.js/redis-provider and @mdf.js/socket-client-provider for more details.
                            • -
                            -
                          • -
                          • Default value: undefined
                          • -
                          -
                        • -
                        • -

                          observabilityOptions (ObservabilityOptions): Observability configuration options.

                          -
                            -
                          • -

                            Properties:

                            -
                              -
                            • port (number): Port of the observability server.
                            • -
                            • primaryPort (number): Primary port of the observability server in cluster mode, all the request to services over the port will be redirected to the primary port transparently.
                            • -
                            • host (string): Host of the observability server.
                            • -
                            • isCluster (boolean): Flag indicating that the service is running in cluster mode. If the service is running in cluster mode, the observability server will be started in all the instances of the cluster, but only the primary instance will be able to receive commands.
                            • -
                            • includeStack (boolean): Flag indicating that the stack trace should be included in the error register.
                            • -
                            • clusterUpdateInterval (number): Interval of time in milliseconds to update the cluster information.
                            • -
                            • maxSize (number): Maximum size of the error register.
                            • -
                            -
                          • -
                          • -

                            Default value:

                            -
                            {
                            primaryPort: 9080,
                            host: 'localhost',
                            isCluster: false,
                            includeStack: false,
                            clusterUpdateInterval: 10000,
                            maxSize: 100,
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          loggerOptions (LoggerOptions): Logger Options. If provided, a logger instance from the @mdf.js/logger package will be created and used by the application in all the internal services of the Application Wrapper. At the same time, the logger is exposed to the application to be used in the application services. If this options is not provided, a Debug logger will be used internally, but it will not be exposed to the application.

                          -
                            -
                          • -

                            Properties: check the documentation of the package @mdf.js/logger.

                            -
                          • -
                          • -

                            Default value:

                            -
                            {
                            console: {
                            enabled: true,
                            level: 'info',
                            },
                            file: {
                            enabled: false,
                            level: 'info',
                            },
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          retryOptions (RetryOptions): Retry options. If provided, the application will use this options to retry to start the services/resources registered in the Application Wrapped instance. If this options is not provided, the application will not retry to start the services/resources.

                          -
                            -
                          • -

                            Properties: check the documentation of the package @mdf.js/utils.

                            -
                          • -
                          • -

                            Default value:

                            -
                            {
                            attempts: 3,
                            maxWaitTime: 10000,
                            timeout: 5000,
                            waitTime: 1000,
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          configLoaderOptions (ConfigLoaderOptions): Configuration loader options. These options is used to load the configuration information of the application that is been wrapped by the Application Wrapper. This configuration could be loaded from files or environment variables, or even both.

                          -

                          To understand the configuration loader options, check the documentation of the package @mdf.js/service-setup-provider.

                          -
                          -

                          Note: Use different files for Application Wrapper configuration and for your own services to avoid conflicts.

                          -
                          -
                            -
                          • -

                            Properties: check the documentation of the package @mdf.js/service-setup-provider.

                            -
                          • -
                          • -

                            Default value:

                            -
                            {
                            configFiles: ['./config/custom/*.*'],
                            presetFiles: ['./config/custom/presets/*.*'],
                            schemaFiles: ['./config/custom/schemas/*.*'],
                            preset: process.env['CONFIG_CUSTOM_PRESET'] || process.env['CONFIG_SERVICE_REGISTRY_PRESET'],
                            useEnvironment: false,
                            loadReadme: false,
                            loadPackage: false,
                            } -
                            - -
                          • -
                          -
                        • -
                        -

                        These options are used to provide custom configuration to the services that are been wrapped by the Service Registry. These options are accessible through the settings.custom or customSettings property of the ServiceRegistry object. The options that you provide here will be merged with the rest of the configuration loaded based on the configLoaderOptions, being the last one the one that will override the rest of the configuration, in this way, you can create your own way to select the configuration that you want to use in your services, besides the use of the integrated @mdf.js/service-setup-provider for this purpose.

                        -
                          -
                        • Properties: -
                            -
                          • errors (ErrorRecord[]): Errors recorded by the application, the maximum size of the error register is defined by the maxSize option in the observabilityOptions.
                          • -
                          • health (Layer.App.Health): Health object, check the documentation of the package @mdf.js/core for more details.
                          • -
                          • status (Health.Status): Service status, check the documentation of the package @mdf.js/core for more details.
                          • -
                          • serviceRegistrySettings (ServiceRegistrySettings): final configuration parameters which are used by the service registry.
                          • -
                          • customSettings (CustomSettings): final result of the custom parameters.
                          • -
                          • settings (ServiceSetting): final result of the settings.
                          • -
                          -
                        • -
                        • Methods: -
                            -
                          • register(resource: Layer.Observable | Layer.Observable[]): void: Register a resource or an array of resources to the observability services of the application. If the resource fullfil the Layer.App.Resource or Layer.App.Service interfaces, the resource will be started when the application starts. Check the documentation of the package @mdf.js/core for more details.
                          • -
                          • get<T>(path: string | string[], defaultValue: T): T | undefined: Get a configuration value by path from the settings. If the path is not found, the default value will be returned.
                          • -
                          • get<P extends keyof CustomSettings>(key: P, defaultValue: CustomSettings[P]): CustomSettings[P] | undefined: Get a custom configuration value by key from the custom settings. If the key is not found, the default value will be returned.
                          • -
                          • async start(): Promise<void>: Start the application, this method will start all the resources registered in the application. If the application is running in cluster mode, only the primary instance will start the resources.
                          • -
                          • async stop(): Promise<void>: Stop the application, this method will stop all the resources registered in the application. If the application is running in cluster mode, only the primary instance will stop the resources.
                          • -
                          -
                        • -
                        • Events: -
                            -
                          • on(event: 'command', listener: (job: CommandJobHandler) => void): this: Event emitted when a command is received by the consumer. The event listener will receive a CommandJobHandler object with the command information. See below for more information.
                          • -
                          -
                        • -
                        -

                        By default the observability server is started in the port 9080, over the localhost. The observability server exposes the following endpoints:

                        -
                          -
                        • http://${host}:${port}/v${release}/health: Health check endpoint, returns the health of the service.
                        • -
                        • http://${host}:${port}/v${release}/metrics?json=true: Metrics endpoint, returns the metrics of the service in Prometheus format, if the query parameter json=true is provided, the metrics will be returned in JSON format.
                        • -
                        • http://${host}:${port}/v${release}/registry: Errors endpoint, returns the errors registered by the service, the maximum size of the error register is defined by the maxSize option in the observabilityOptions.
                        • -
                        -

                        If a consumer adapter is configured, the observability server will expose the following endpoints:

                        -
                          -
                        • http://${host}:${port}/v${release}/openc2/command: OpenC2 command interface, used to send OpenC2 commands to the service. See below for more information.
                        • -
                        • http://${host}:${port}/v${release}/openc2/jobs: OpenC2 jobs interface, used to query the jobs registered by the service.
                        • -
                        • http://${host}:${port}/v${release}/openc2/pendingJobs: OpenC2 pending jobs interface, used to query the pending jobs registered by the service.
                        • -
                        • http://${host}:${port}/v${release}/openc2/messages: OpenC2 messages interface, used to query the messages registered by the service.
                        • -
                        -

                        If the user register a service that fullfil the Layer.App.Service interface, including the Links and Router properties, the service will be started when the application starts, and the service will be exposed in the observability endpoints. Check the documentation of the package @mdf.js/core for more details.

                        -

                        The @mdf.js/service-registry module use the OpenC2 as Command and Control Interface (CCI).

                        -

                        This interface are based on the two modules of @mdf.js:

                        -
                          -
                        • @mdf.js/openc2-core: module that implement the OpenC2 core specification for Consumer, Provider and Gateway entities, not attached to any transport layer.
                        • -
                        • @mdf.js/openc2: module that implement a tooling interface, to allow the use of OpenC2 entities over several transport layers: MQTT, Redis Pub/Sub, AMQP, SocketIO ...
                        • -
                        -

                        Please check the documentation of the packages @mdf.js/openc2 and @mdf.js/openc2-core, and the OpenC2 specification for more details.

                        -
                          -
                        • CONFIG_CUSTOM_PRESET: Default custom config loader options
                        • -
                        • CONFIG_SERVICE_REGISTRY_PRESET: Default service registry config loader options
                        • -
                        • NODE_APP_INSTANCE: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics
                        • -
                        • NODE_APP_INSTANCE: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics
                        • -
                        • NODE_APP_INSTANCE: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Classes

                        Interfaces

                        Type Aliases

                        diff --git a/docs/modules/_mdf_js_service_setup_provider.Setup.html b/docs/modules/_mdf_js_service_setup_provider.Setup.html deleted file mode 100644 index 984a3dd1..00000000 --- a/docs/modules/_mdf_js_service_setup_provider.Setup.html +++ /dev/null @@ -1,5 +0,0 @@ -Setup | @mdf.js

                        References

                        Interfaces

                        Type Aliases

                        Variables

                        References

                        Renames and re-exports ConfigManager
                        diff --git a/docs/modules/_mdf_js_service_setup_provider.html b/docs/modules/_mdf_js_service_setup_provider.html deleted file mode 100644 index b6adcf3e..00000000 --- a/docs/modules/_mdf_js_service_setup_provider.html +++ /dev/null @@ -1,140 +0,0 @@ -@mdf.js/service-setup-provider | @mdf.js

                        Module @mdf.js/service-setup-provider

                        @mdf.js/service-setup-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/service-setup-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        @mdf.js/service-setup-provider is a versatile tool designed for handling, validating, and managing different sources of configuration information in Node.js applications. It provides robust support for environment-specific configurations, presets, and schema validation, making it an essential utility for projects that require dynamic configuration management. It supports configurations in JSON, YAML, TOML, and .env file formats and environment variables, allowing developers to define and manage configurations in a structured and consistent manner.

                        -

                        This module is designed and developed to facilitate the deployment of applications that operate in container environments where the same application is deployed in different contexts, such as development, testing, production, installation type A, installation type B, etc. This is the case for applications that are deployed in container environments, like Kubernetes, Docker, etc., especially in Edge Computing environments, where the application is deployed in different geographical locations, with different network configurations, hardware, etc.

                        -

                        In each context, the application needs to be configured differently, with configuration errors being common, especially in applications where fine-tuning of operation is achieved through a large number of configuration variables.

                        -

                        In these environments, it would be ideal to have a series of predefined configurations that fit each context, so that in the application deployment process, one only needs to choose the context (the predefined configuration) they wish to use, without the need for manual adjustments in the configuration variables.

                        -

                        At the same time, it must be possible, especially for the configuration of secrets, to have the ability to adjust environment variables that are loaded into the container at the time of execution, so that the configuration of secrets is not found in the application's source code or in configuration files, but the final result is the union of both configurations.

                        -
                          -
                        • Load and merge configuration files from various formats (JSON, YAML, TOML, .env), allowing a hierarchical configuration structure, merging configurations from different sources.
                        • -
                        • Load environment variables and merge with the rest of the configuration sources, handling environment-specific configurations with ease.
                        • -
                        • Validate configurations against a defined schema, using JSON Schema files.
                        • -
                        • Built-in express.js router for exposing configuration details over HTTP.
                        • -
                        -

                        Using npm:

                        -
                        npm install @mdf.js/service-setup-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/service-setup-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        This module is developed as a @mdf.js Provider so that it can be used easily in any application, both in the @mdf.js environment and in any other Node.js application.

                        -

                        In order to use this module, your should use the Factory exposed and create an instance using the create method:

                        -
                        import { Factory } from '@mdf.js/service-setup-provider';

                        const default = Factory.create(); // Create a new instance with default options

                        const custom = Factory.create({
                        config: {...} // Custom options
                        name: 'custom' // Custom name
                        useEnvironment: true // Use environment variables
                        logger: myLoggerInstance // Custom logger
                        }); -
                        - -

                        The configuration options (config) are the following:

                        -
                          -
                        • -

                          configFiles: List of configuration files to be loaded. The entries could be a file path or glob pattern. All the files will be loaded and merged in the order they are founded. The result of the merge will be used as the final configuration.

                          -

                          Some examples:

                          -
                          ['./config/*.json']
                          ['./config/*.json', './config/*.yaml']
                          ['./config/*.json', './config/*.yaml', './config/*.yml'] -
                          - -
                        • -
                        • -

                          presetFiles: List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset.

                          -

                          Some examples:

                          -
                          ['./config/presets/*.json']
                          ['./config/presets/*.json', './config/presets/*.yaml']
                          ['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml'] -
                          - -
                        • -
                        • -

                          envPrefix: Prefix or prefixes to use on configuration loading from the environment variables. The prefix will be used to filter the environment variables. The prefix will be removed from the environment variable name and the remaining part will be used as the configuration property name. The configuration property name will be converted to camel case. Environment variables will override the configuration from the configuration files.

                          -

                          Some examples:

                          -
                          `MY_APP_` // as single prefix
                          ['MY_APP_', 'MY_OTHER_APP_'] // as array of prefixes
                          { MY_APP: 'myApp', MY_OTHER_APP: 'myOtherApp' } // as object with prefixes -
                          - -
                        • -
                        -
                        -

                        Note: is important not misunderstand the envPrefix option and useEnvironment option of the Factory.

                        -
                          -
                        • envPrefix: this option is used to filter the environment variables that will be used to override the configuration from the configuration files. The envPrefix will affect only to the result of the final configuration object that this module is going to create.
                        • -
                        • useEnvironment: this option of the create method will be used to indicate if the environment variables will be used, or not, to override the configuration of this module.
                        • -
                        -
                        -
                          -
                        • schemaFiles: List of files with JSON schemas used to validate the configuration. The entries could be a file path or glob pattern.
                        • -
                        -

                        In this point we have:

                        -
                          -
                        • A config object as result of the merge of the configuration files.
                        • -
                        • A collection of presets objects as result of the merge of the preset files.
                        • -
                        • A environment object as result of parse the environment variables based on the envPrefix option.
                        • -
                        • A collection of schemas objects as result of the schema files.
                        • -
                        -

                        What we have to configure now is if we want to use a preset file and which one and if we want to validate the result based in a JSON schema. For this we have the following options:

                        -
                          -
                        • preset: Preset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used.
                        • -
                        • schema: Schema to be used to validate the configuration. If none is indicated, the configuration will not be validated. The schema name should be the same as the file name without the extension.
                        • -
                        • checker: DoorKeeper instance to be used to validate the configuration. If none is indicated, the setup instance will be try to create a new DoorKeeper instance using the schema files indicated in the options. If the schema files are not indicated, the configuration will not be validated.
                        • -
                        • base: Object to be used as base and main configuration options. The configuration will be merged with the configuration from the configuration files. This object will override the configuration from the configuration files and the environment variables. The main reason of this option is to allow the user to define some configuration in the code and let the rest of the configuration to be loaded, using the Configuration Manager as unique source of configuration.
                        • -
                        • default: Object to be used as default configuration options. The configuration will be merged with the configuration from the configuration files, the environment variables and the base option. This object will be used as the default configuration if no other configuration is found.
                        • -
                        -

                        The preset option is used to indicate which preset will be used as the base configuration. The preset name should be the same as the file name without the extension. The preset will be merged with the configuration from the configuration files and the environment variables. The preset will override the configuration from the configuration files and the environment variables will override the preset.

                        -

                        Once the instance is created, you can access to the ConfigManager instance using the client property of the Provider. The ConfigManager instance has the following methods and properties:

                        -
                          -
                        • Properties: -
                            -
                          • defaultConfig: configuration object with the result of the merge of the configuration files.
                          • -
                          • envConfig: configuration object with the result of the merge the environment variables.
                          • -
                          • presets: Collection of presets objects with the result of the merge of the preset files.
                          • -
                          • preset: selected present.
                          • -
                          • schema: selected schema.
                          • -
                          • nonDisclosureConfig: configuration object with the result of the merge of the configuration files, the preset WITHOUT the environment variables. In environments variables is where we should store the secrets.
                          • -
                          • config: Configuration object with the result of the merge of the configuration files, the preset and the environment variables.
                          • -
                          • isErrored: boolean that indicates if the configuration is valid or not.
                          • -
                          • error: a Multi instance with the errors found in the configuration validation if the configuration is not valid.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_SERVICE_SETUP_PRESET_FILES (default: './config/presets/*.*'): List of files with preset options to be loaded. The entries could be a file path or glob pattern. The first part of the file name will be used as the preset name. The file name should be in the format of presetName.config.json or presetName.config.yaml. The name of the preset will be used to merge different files in order to create a single preset.
                        • -
                        • CONFIG_SERVICE_SETUP_SCHEMA_FILES (default: './config/schemas/*.*'): List of files with JSON schemas used to validate the configuration. The entries could be a file path or glob pattern.
                        • -
                        • CONFIG_SERVICE_SETUP_CONFIG_FILES (default: './config/*.*'): List of configuration files to be loaded. The entries could be a file path or glob pattern. All the files will be loaded and merged in the order they are founded. The result of the merge will be used as the final configuration.
                        • -
                        • CONFIG_SERVICE_SETUP_PRESET (default: undefined): Preset to be used as configuration base, if none is indicated, or the indicated preset is not found, the configuration from the configuration files will be used.
                        • -
                        • CONFIG_SERVICE_SETUP_SCHEMA (default: undefined): Schema to be used to validate the configuration. If none is indicated, the configuration will not be validated. The schema name should be the same as the file name without the extension.
                        • -
                        • CONFIG_SERVICE_SETUP_ENV_PREFIX (default: undefined): Prefix or prefixes to use on configuration loading from the environment variables. The prefix will be used to filter the environment variables. The prefix will be removed from the environment variable name and the remaining part will be used as the configuration property name. The configuration property name will be converted to camel case. Environment variables will override the configuration from the configuration files.
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        Classes

                        diff --git a/docs/modules/_mdf_js_socket_client_provider.SocketIOClient.html b/docs/modules/_mdf_js_socket_client_provider.SocketIOClient.html deleted file mode 100644 index 3428025e..00000000 --- a/docs/modules/_mdf_js_socket_client_provider.SocketIOClient.html +++ /dev/null @@ -1,7 +0,0 @@ -SocketIOClient | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_socket_client_provider.html b/docs/modules/_mdf_js_socket_client_provider.html deleted file mode 100644 index f5434cf4..00000000 --- a/docs/modules/_mdf_js_socket_client_provider.html +++ /dev/null @@ -1,56 +0,0 @@ -@mdf.js/socket-client-provider | @mdf.js

                        Module @mdf.js/socket-client-provider

                        @mdf.js/socket-client-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/socket-client-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        Socket client provider for @mdf.js based on socket.io-client.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/socket-client-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/socket-client-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Check the status of the connection to the socket-io server based on the connection and disconnection events from the socket-io client. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and connected to the server, error if the provider is running but disconnected from the server.
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_SOCKET_IO_CLIENT_URL (default: 'http://localhost:8080'): URL of the server
                        • -
                        • CONFIG_SOCKET_IO_CLIENT_PATH (default: '/socket.io'): Path where the server will listen
                        • -
                        • CONFIG_SOCKET_IO_CLIENT_TRANSPORTS (default: ['websocket']): Transports to use
                        • -
                        • CONFIG_SOCKET_IO_CLIENT_CA_PATH (default: undefined): CA file path
                        • -
                        • CONFIG_SOCKET_IO_CLIENT_CLIENT_CERT_PATH (default: undefined): Client cert file path
                        • -
                        • CONFIG_SOCKET_IO_CLIENT_CLIENT_KEY_PATH (default: undefined): Client key file path
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_socket_server_provider.SocketIOServer.html b/docs/modules/_mdf_js_socket_server_provider.SocketIOServer.html deleted file mode 100644 index 16e57fd1..00000000 --- a/docs/modules/_mdf_js_socket_server_provider.SocketIOServer.html +++ /dev/null @@ -1,7 +0,0 @@ -SocketIOServer | @mdf.js

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        diff --git a/docs/modules/_mdf_js_socket_server_provider.html b/docs/modules/_mdf_js_socket_server_provider.html deleted file mode 100644 index 5bd1c8cb..00000000 --- a/docs/modules/_mdf_js_socket_server_provider.html +++ /dev/null @@ -1,56 +0,0 @@ -@mdf.js/socket-server-provider | @mdf.js

                        Module @mdf.js/socket-server-provider

                        @mdf.js/socket-server-provider

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/socket-server-provider

                        -
                        Typescript tools for development
                        - -
                        - -

                        Socket server provider for @mdf.js based on socket.io.

                        -

                        Using npm:

                        -
                        npm install @mdf.js/socket-server-provider
                        -
                        - -

                        Using yarn:

                        -
                        yarn add @mdf.js/socket-server-provider
                        -
                        - -

                        Check information about @mdf.js providers in the documentation of the core module @mdf.js/core.

                        -

                        Checks included in the provider:

                        -
                          -
                        • status: Check the status of the server based in the status of the listening port and the error events from the socket-io server. -
                            -
                          • observedValue: actual state of the consumer/producer provider instance [error, running, stopped] based in the last ping event. stopped if the provider is stopped or has not been initialized yet, running if the provider is running and the server is listening and error if the provider is running but the server is not listening.
                          • -
                          • status: pass if the status is running, warn if the status is stopped, fail if the status is error.
                          • -
                          -
                        • -
                        -
                          -
                        • CONFIG_SOCKET_IO_SERVER_PORT (default: 8080): Port where the server will listen
                        • -
                        • CONFIG_SOCKET_IO_SERVER_HOST (default: 'localhost'): Host where the server will listen
                        • -
                        • CONFIG_SOCKET_IO_SERVER_PATH (default: '/socket.io'): Path where the server will listen
                        • -
                        • CONFIG_SOCKET_IO_SERVER_ENABLE_UI (default: true): Enable the UI
                        • -
                        • CONFIG_SOCKET_IO_SERVER_CORS__ORIGIN (default: `/[\s\S]*\/`): CORS origin
                        • -
                        • CONFIG_SOCKET_IO_SERVER_CORS__CREDENTIALS (default: true): CORS credentials
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        Namespaces

                        diff --git a/docs/modules/_mdf_js_tasks.html b/docs/modules/_mdf_js_tasks.html deleted file mode 100644 index 6278e832..00000000 --- a/docs/modules/_mdf_js_tasks.html +++ /dev/null @@ -1,349 +0,0 @@ -@mdf.js/tasks | @mdf.js

                        Module @mdf.js/tasks

                        @mdf.js/tasks

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js

                        -
                        Typescript tools for development
                        - -
                        - -

                        The @mdf.js/tasks package is a set of tools designed to facilite the development of services that require the execution of tasks in a controlled manner. The package is composed of the following elements:

                        -
                          -
                        • Tasks: Single, Group or Sequence are the types of tasks that can be executed, each one extends the TaskHandler class, that provides the basic functionality to manage the task, and include some additional properties and methods to control the execution of specific kind of tasks. -
                            -
                          • Single: A single task that can be executed.
                          • -
                          • Group: A group of tasks that can be executed in parallel.
                          • -
                          • Sequence: A specific sequence needed to execute a concrete task, allowing to define pre, post and finally tasks, besides the main task.
                          • -
                          -
                        • -
                        • Limiter: A class that allows to control the number of tasks that can be executed in parallel.
                        • -
                        • Scheduler: A class that allows to schedule the execution of tasks in a specific time.
                        • -
                        -

                        Each element is designed to be used together with the others, but tasks can be used independently if needed.

                        -

                        To install the @mdf.js/tasks package, you can use the following commands:

                        -
                          -
                        • NPM:
                        • -
                        -
                        npm install @mdf.js/tasks
                        -
                        - -
                          -
                        • Yarn:
                        • -
                        -
                        yarn add @mdf.js/tasks
                        -
                        - -

                        Tasks are the main element of the package, based in the TaskHandler class, that provides the basic functionality to manage the task, and include some additional properties and methods to control the execution of specific kind of tasks. These tasks acts as instances of task execution requests, allowing to control the execution and result. The Single task is the basic task, Groupand Sequence are different ways to execute Single tasks, allowing to resolve more complex scenarios.

                        -

                        As each task type extends the TaskHandler class, let's see the basic properties and methods that are common to all of them:

                        -
                          -
                        • -

                          Properties:

                          -
                            -
                          • -

                            uuid (string): The unique identifier of the task instance.

                            -
                          • -
                          • -

                            taskId (string): The identifier of the task, defined by the user.

                            -
                          • -
                          • -

                            createdAt (Date): The date and time when the task was created.

                            -
                          • -
                          • -

                            priority (number): The priority of the task, used to order the execution of tasks in Limiter or Scheduler.

                            -
                          • -
                          • -

                            weight (number): The weight of the task, used in the Limiter to control the number of tasks that can be executed in parallel.

                            -
                          • -
                          • -

                            metadata (Metadata): The task metadata object that contains all the relevant information about the task and its execution.

                            -
                            /** Metadata of the execution of the task */
                            export interface MetaData {
                            /** Unique task identification, unique for each task */
                            uuid: string;
                            /** Task identifier, defined by the user */
                            taskId: string;
                            /** Status of the task */
                            status: TaskState;
                            /** Date when the task was created */
                            createdAt: string;
                            /** Date when the task was executed in ISO format */
                            executedAt?: string;
                            /** Date when the task was completed in ISO format */
                            completedAt?: string;
                            /** Date when the task was cancelled in ISO format */
                            cancelledAt?: string;
                            /** Date when the task was failed in ISO format */
                            failedAt?: string;
                            /** Reason of failure or cancellation */
                            reason?: string;
                            /** Duration of the task in milliseconds */
                            duration?: number;
                            /** Task priority */
                            priority: number;
                            /** Task weight */
                            weight: number;
                            /** Additional metadata objects, store the metadata information from related tasks in a sequence or group */
                            $meta?: MetaData[];
                            } -
                            - -
                          • -
                          -
                        • -
                        • -

                          Methods:

                          -
                            -
                          • async execute(): Promise<Result>: Executes the task, returning a promise with the result of the execution.
                          • -
                          • async cancel(error?: Crash): void: Cancels the task execution.
                          • -
                          -
                        • -
                        -

                        All the different tasks constructors, besides other parameters, allow to configure the task execution with the following options (TaskOptions):

                        -
                          -
                        • id (string): The identifier of the task, defined by the user, if not provided, a random identifier will be generated.
                        • -
                        • priority (number): The priority of the task, used to order the execution of tasks in Limiter or Scheduler. Default is 0.
                        • -
                        • weight (number): The weight of the task, used in the Limiter to control the number of tasks that can be executed in parallel. Default is 1.
                        • -
                        • retryOptions (RetryOptions): The options to retry the task in case of failure. Check the RetryOptions interface for more information in the @mdf.js/utils package.
                        • -
                        • bind (any): The object to bind the task to, if the task is a method of a class.
                        • -
                        • retryStrategy (RetryStrategy): The strategy to retry the task in case of execute method being called again. Possible values are: -
                            -
                          • retry (RETRY_STRATEGY.RETRY): The task will allow to retry the execution again if it fails, updating the metadata in each retry.
                          • -
                          • failAfterSuccess (RETRY_STRATEGY.FAIL_AFTER_SUCCESS): The task will allow to be executed again if it fails, but it will rejects if there are more retries before the success.
                          • -
                          • failAfterExecuted (RETRY_STRATEGY.FAIL_AFTER_EXECUTED): The task will allow only one execution, if it fails, it will fail in every retry.
                          • -
                          • notExecAfterSuccess (RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS): The task will resolve the result of first successful execution, if it fails, it will allow to be executed again.
                          • -
                          -
                        • -
                        -

                        The Single task is the basic task, it has not more options than the TaskHandler class, but it can be used to execute any kind of task, as a function or a method of a class. The Single task can be used to execute a single task, and it can be used in combination with the Limiter or Scheduler classes to control the execution of tasks.

                        -
                        import { Single, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        // Any kind of promise can be used as task
                        function task(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        // Or a method of a class
                        class MyClass {
                        task(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        }

                        const myInstance = new MyClass();

                        // A task can be created with a function
                        const unBindedTask = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        unBindedTask.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        });

                        // Or binded to a class instance
                        const bindedTask = new Single(myInstance.task, 5, {
                        id: 'task2',
                        bind: myInstance,
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        bindedTask.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        }); -
                        - -

                        The Group task is a set of tasks that can be executed in order. The Result of the execution of the group is an array with the results of each task, and the $meta property of the metadata object contains the metadata of each task.

                        -

                        The constructor of the Group has the next parameters:

                        -
                          -
                        • tasks (TaskHandler[]): The tasks to be executed in the group.
                        • -
                        • options (TaskOptions): The options to configure the group task execution.
                        • -
                        • atLeastOne (boolean): If true, the group will resolve the result if at least one task is resolved, if false, all the tasks must be resolved to resolve the group.
                        • -
                        -
                        import { Group, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        const tasks = [
                        new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ];

                        const group = new Group(tasks, {
                        id: 'group1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        group.on('done', (uuid: string, result: number[], meta: Metadata, error?: Crash) => {
                        console.log('Group done', uuid, result, meta, error);
                        }); -
                        - -

                        The Sequence task is a special king of task that need to execute a sequence of tasks in a specific order. The Sequence task allows to define pre, post and finally tasks, besides the main task. The Result of the execution of the sequence is the result of the main task, and the $meta property of the metadata object contains the metadata of each task.

                        -

                        The constructor of the Sequence has the next parameters:

                        -
                          -
                        • pattern (SequencePattern): The pattern of the sequence, that defines the pre, post, main and finally tasks: -
                            -
                          • pre (TaskHandler[]): The tasks to be executed before the main task.
                          • -
                          • task (TaskHandler): The main task to be executed.
                          • -
                          • post (TaskHandler[]): The tasks to be executed after the main task, if the main task fails, the post tasks will not be executed.
                          • -
                          • finally (TaskHandler[]): The tasks to be executed at the end of the sequence, even if the main task fails.
                          • -
                          -
                        • -
                        • options (TaskOptions): The options to configure the sequence task execution.
                        • -
                        -
                        import { Sequence, Metadata } from '@mdf.js/tasks';
                        import { Crash } from '@mdf.js/crash';

                        const sequence = new Sequence(
                        {
                        pre: [
                        new Single(task, 5, {
                        id: 'pre1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        task: new Single(task, 10, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        post: [
                        new Single(task, 15, {
                        id: 'post1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        finally: [
                        new Single(task, 20, {
                        id: 'finally1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }),
                        ],
                        },
                        {
                        id: 'sequence1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        }
                        );

                        sequence.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Sequence done', uuid, result, meta, error);
                        }); -
                        - -

                        The Limiter class allows to control the execution of tasks, limiting the number of tasks that can be executed in parallel, the order of the execution based in the priority of the tasks, the cadence of the execution and "throughput", controlling the number of tasks that can be executed in a specific time.

                        -

                        The Limiter accepts tasks of any kind, Single, Group or Sequence, allowing to schedule the execution of the tasks or execute them, taking always into account the Limiter configuration.

                        -

                        In order to create a new Limiter instance, the constructor accepts a LimiterOptions object with the following properties:

                        -
                          -
                        • concurrency (number): The maximum number of concurrent jobs. Default is 1.
                        • -
                        • delay (number): Delay between each job in milliseconds. Default is 0. For concurrency = 1, the delay is applied after each job is finished. For concurrency > 1, if the actual number of concurrent jobs is less than concurrency, the delay is applied after each job is finished, otherwise, the delay is applied after each job is started.
                        • -
                        • retryOptions (RetryOptions): Set the default options for the retry process of the jobs. Default is undefined. Check the RetryOptions interface for more information in the @mdf.js/utils package.
                        • -
                        • autoStart (boolean): Set whether the limiter should start to process the jobs automatically. Default is true.
                        • -
                        • highWater (number): The maximum number of jobs in the queue. Default is Infinity.
                        • -
                        • strategy (Strategy): The strategy to use when the queue length reaches highWater. Default is 'leak'. Possible values are: -
                            -
                          • leak (STRATEGY.LEAK): When adding a new job to a limiter, if the queue length reaches highWater, drop the oldest job with the lowest priority. This is useful when jobs that have been waiting for too long are not important anymore. If all the queued jobs are more important (based on their priority value) than the one being added, it will not be added.
                          • -
                          • overflow (STRATEGY.OVERFLOW): When adding a new job to a limiter, if the queue length reaches highWater, do not add the new job. This strategy totally ignores priority levels.
                          • -
                          • overflow-priority (STRATEGY.OVERFLOW_PRIORITY): Same as LEAK, except it will only drop jobs that are less important than the one being added. If all the queued jobs are as or more important than the new one, it will not be added.
                          • -
                          • block (STRATEGY.BLOCK): When adding a new job to a limiter, if the queue length reaches highWater, the limiter falls into "blocked mode". All queued jobs are dropped and no new jobs will be accepted until the limiter unblocks. It will unblock after penalty milliseconds have passed without receiving a new job. penalty is equal to 15 * minTime (or 5000 if minTime is 0) by default. This strategy is ideal when bruteforce attacks are to be expected. This strategy totally ignores priority levels.
                          • -
                          -
                        • -
                        • penalty (number): The penalty for the BLOCK strategy in milliseconds. Default is 0.
                        • -
                        • bucketSize (number): The bucket size for the rate limiter. Default is 0. If the bucket size is 0, only concurrency and delay will be used to limit the rate of the jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to limit the rate of the jobs. The bucket size is the maximum number of tokens that can be consumed in the interval. The interval is defined by the tokensPerInterval and interval properties.
                        • -
                        • tokensPerInterval (number): Define the number of tokens that will be added to the bucket at the beginning of the interval. Default is 1.
                        • -
                        • interval (number): Define the interval in milliseconds. Default is 1000.
                        • -
                        -
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        }); -
                        - -

                        The Limiter class allows to:

                        -
                          -
                        • schedule the execution of tasks, that means that the tasks are added to the queue, and they will be executed when the limiter is ready to process them. When the task is executed two events: done and an event with the taskId, both of them with the same information: -
                            -
                          • on('done' | taskId, listener: (uuid: string, result: Result, meta: MetaData, error?: Crash) => void): this:
                          • -
                          • uuid: The unique identifier of the task instance.
                          • -
                          • result: The result of the task execution.
                          • -
                          • meta: The metadata of the task execution.
                          • -
                          • error: The error in case of failure.
                          • -
                          -
                        • -
                        • execute the task, that will wait until the limiter is ready to process the task, and execute it, resolving the result of the task execution.
                        • -
                        -

                        There are several methods to interact with the limiter and control the execution of the tasks:

                        -
                          -
                        • start(): void: Start the limiter, allowing to process the tasks in the queue. If the limiter is already started, it will not do anything. If autoStart is true, the limiter will start automatically when a task is added to the queue.
                        • -
                        • stop(): void: Stop the limiter, preventing to process the tasks in the queue. If the limiter is already stopped, it will not do anything.
                        • -
                        • waitUntilEmpty(): Promise<void>: Wait until the queue is empty.
                        • -
                        • clear(): void: Clear the queue, removing all the tasks in the queue.
                        • -
                        -

                        And several properties to get information about the limiter:

                        -
                          -
                        • size (number): The number of tasks in the queue.
                        • -
                        • pending (number): The number of tasks that are being executed.
                        • -
                        • options (LimiterOptions): The options of the limiter.
                        • -
                        -

                        In order to create more complex scenarios, the Limiter class allows to use pipe limiters to control the execution of tasks in a more complex way. This option allows, for example, to create several limiters to pull information from different sources, ensuring that this sources are not overloaded, and pipe them to a main limiter that will protect the own system from being overloaded.

                        -

                        Using schedule method:

                        -
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        });

                        const task1 = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        const task2 = new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        limiter.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash) => {
                        console.log('Task done', uuid, result, meta, error);
                        });

                        limiter.schedule(task1);
                        limiter.schedule(task2); -
                        - -

                        Using execute method:

                        -
                        import { Limiter, LimiterOptions } from '@mdf.js/tasks';

                        const limiter = new Limiter({
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        });

                        const task1 = new Single(task, 5, {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        const task2 = new Single(task, 10, {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        });

                        limiter.execute(task1).then(result => {
                        console.log('Task done', result);
                        });
                        limiter.execute(task2).then(result => {
                        console.log('Task done', result);
                        }); -
                        - -

                        The Scheduler class allows to schedule the execution of tasks based on resources and a polling times, this means periodically, controlling the execution of the tasks by the use of a Limiter instance per resource, piped with a Limiter for the Scheduler instance.

                        -

                        The Scheduler creates two types of cycles, a fast cycle and a slow cycle, per polling group and resource. The fast cycle is executed every time the polling group is reached, and the slow cycle is executed after slowCycleRatio fast cycles. The Scheduler class allows to control the execution of the tasks, and provides a set of metrics to monitor the execution of the tasks.

                        -

                        In order to create a new Scheduler instance, the constructor accepts the next parameters:

                        -
                          -
                        • name (string): The name of the scheduler.
                        • -
                        • options (SchedulerOptions): The options to configure the scheduler: -
                            -
                          • logger (Logger): The logger instance to use. If not provided, a default DebugLogger from the @mdf.js/logger package will be used with the name mdf:scheduler:${name}.
                          • -
                          • limiterOptions (LimiterOptions): The options to configure the limiter of the scheduler.
                          • -
                          • resources (ResourcesConfigObject): an object with an entry for each resource, where the key is the name of the resource, and the value is a ResourceConfigEntry with the following properties: -
                              -
                            • limiterOptions (LimiterOptions): The options to configure the limiter of the resource.
                            • -
                            • pollingGroups (object): A object with a entry for each polling group, where the key is the name of the group, and the value is a TaskBaseConfig array with the tasks to be executed in the group. The keys of this object should be of the type PollingGroup, this is a string with the format ${number}d, ${number}h, ${number}m, ${number}s, ${number}ms, where ${number} is the number of days, hours, minutes, seconds or milliseconds to wait between each polling. -The TaskBaseConfig could be a SingleTaskBaseConfig, a GroupTaskBaseConfig or a SequenceTaskBaseConfig object, with the following properties: -
                                -
                              • SingleTaskBaseConfig: -
                                  -
                                • task (TaskAsPromise): Promise to be executed.
                                • -
                                • taskArgs (any[]): Arguments to be passed to the task.
                                • -
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • -
                                -
                              • -
                              • GroupTaskBaseConfig: -
                                  -
                                • tasks (SingleTaskBaseConfig[]): Array of SingleTaskBaseConfig objects.
                                • -
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • -
                                -
                              • -
                              • SequenceTaskBaseConfig: -
                                  -
                                • pattern (SequencePattern): The pattern of the sequence, that defines the pre, post, main and finally tasks: -
                                    -
                                  • pre (SingleTaskBaseConfig[]): The tasks to be executed before the main task.
                                  • -
                                  • task (SingleTaskBaseConfig): The main task to be executed.
                                  • -
                                  • post (SingleTaskBaseConfig[]): The tasks to be executed after the main task, if the main task fails, the post tasks will not be executed.
                                  • -
                                  • finally (SingleTaskBaseConfig[]): The tasks to be executed at the end of the sequence, even if the main task fails.
                                  • -
                                  -
                                • -
                                • options (TaskOptions): a TaskOptions object where the id property is mandatory.
                                • -
                                -
                              • -
                              -
                            • -
                            -
                          • -
                          • slowCycleRatio (number): number of fast cycles to be executed before a slow cycle is executed. Default is 3.
                          • -
                          • cyclesOnStats (number): number of cycles to be included in the statistics. Default is 10.
                          • -
                          -
                        • -
                        -

                        The Scheduler has generic parameters in order to be typed:

                        -
                          -
                        • Result (Result): The type of the result of the tasks. If not provided, the result will be any.
                        • -
                        • Binding (Binding): The type of the object to bind the tasks to. If not provided, the binding will be any.
                        • -
                        • PollingGroups (PollingGroup): The available polling groups. If not provided, the polling groups will be DefaultPollingGroups: '1d', '12h', '6h', '6h', '1h', '30m', '15m', '10m', '5m', '1m', '30s', '10s', '5s'.
                        • -
                        -
                        import { Scheduler, SchedulerOptions } from '@mdf.js/tasks';

                        class MyClass {
                        constructor(private readonly resource: string) {};
                        task1(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 2);
                        }, 1000);
                        });
                        }
                        task2(value: number): Promise<number> {
                        return new Promise(resolve => {
                        setTimeout(() => {
                        resolve(value * 3);
                        }, 1000);
                        });
                        }
                        }

                        const resource1 = new MyClass('resource1');
                        const resource2 = new MyClass('resource2');
                        type MyPollingGroups = '5m' | '1m';

                        const scheduler = new Scheduler<number, MyClass, MyPollingGroups>('myScheduler', {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        resources: {
                        resource1: {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource1.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource1.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        },
                        resource2: {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource2.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource2.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        },
                        },
                        }); -
                        - -

                        New resources can be added to the scheduler using the addResource or addResources methods, and deleted using the dropResource method, in all the cases the Scheduler should be stopped, in other case the method will throw an error. The resources can be cleared using the cleanup method.

                        -
                        scheduler.addResource('resource3', {
                        limiterOptions: {
                        concurrency: 2,
                        delay: 1000,
                        highWater: 10,
                        strategy: 'leak',
                        penalty: 5000,
                        bucketSize: 10,
                        tokensPerInterval: 1,
                        interval: 1000,
                        },
                        pollingGroups: {
                        '5m': [
                        {
                        task: resource2.task1,
                        taskArgs: [5],
                        options: {
                        id: 'task1',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        '1m': [
                        {
                        task: resource2.task2,
                        taskArgs: [10],
                        options: {
                        id: 'task2',
                        priority: 1,
                        weight: 1,
                        retryOptions: { attempts: 3 },
                        },
                        },
                        ],
                        },
                        });

                        scheduler.dropResource('resource3');
                        scheduler.cleanup(); -
                        - -

                        The Scheduler class allows to start and stop the scheduler, controlling the execution of the tasks:

                        -
                          -
                        • async start(): Promise<void>: Start the scheduler, allowing to process the tasks in the polling groups. If the scheduler is already started, it will not do anything.
                        • -
                        • async stop(): Promise<void>: Stop the scheduler, preventing to process the tasks in the polling groups. If the scheduler is already stopped, it will not do anything.
                        • -
                        • async close(): Promise<void>: Close the scheduler, stopping the scheduler and clearing the polling groups.
                        • -
                        -

                        Every time a task is executed, the done event is emitted with the following parameters:

                        -
                          -
                        • uuid: The unique identifier of the task instance.
                        • -
                        • result: The result of the task execution.
                        • -
                        • meta: The metadata of the task execution.
                        • -
                        • error: The error in case of failure.
                        • -
                        • resource: The name of the resource where the task was executed.
                        • -
                        -
                        scheduler.on('done', (uuid: string, result: number, meta: Metadata, error?: Crash, resource: string) => {
                        console.log('Task done', uuid, result, meta, error, resource);
                        }); -
                        - -

                        The Scheduler class implements the Layer.App.Service interface, so it can be used with the @mdf.js/service-registry package to monitor the scheduler. The Scheduler class collect the following metrics for each resource and polling group:

                        -
                          -
                        • scanTime (Date): The date and time when the scan was performed.
                        • -
                        • cycles (number): The number of cycles performed.
                        • -
                        • overruns (number): The number of cycles with overruns.
                        • -
                        • consecutiveOverruns (number): The number of consecutive overruns.
                        • -
                        • averageCycleDuration (number): The average cycle duration in milliseconds.
                        • -
                        • maxCycleDuration (number): The maximum cycle duration in milliseconds.
                        • -
                        • minCycleDuration (number): The minimum cycle duration in milliseconds.
                        • -
                        • lastCycleDuration (number): The last cycle duration in milliseconds.
                        • -
                        • inFastCycleTasks (number): The number of tasks included on the regular cycle.
                        • -
                        • inSlowCycleTasks (number): The number of tasks included on the slow cycle. This cycle is executed after slowCycleRatio fast cycles.
                        • -
                        • inOffCycleTasks (number): The number of tasks included on the off cycle, these are not executed.
                        • -
                        • pendingTasks (number): The number of tasks in execution in this moment.
                        • -
                        -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -

                        Index

                        @mdf.js/tasks

                        Other

                        diff --git a/docs/modules/_mdf_js_utils.html b/docs/modules/_mdf_js_utils.html deleted file mode 100644 index 216beca9..00000000 --- a/docs/modules/_mdf_js_utils.html +++ /dev/null @@ -1,192 +0,0 @@ -@mdf.js/utils | @mdf.js

                        Module @mdf.js/utils

                        @mdf.js/utils

                        Node Version -Typescript Version -Known Vulnerabilities

                        - -

                        -

                        - netin -
                        -

                        -

                        Mytra Development Framework - @mdf.js/utils

                        -
                        Collection of tools useful for several different tasks within the @mdf.js ecosystem
                        - -
                        - -

                        The @mdf.js/utils module is a collection of tools useful for several different tasks within the @mdf.js ecosystem. It is a collection of utilities that are used in different parts of the framework, such as the API, the CLI, the documentation...

                        -

                        The list of utilities are:

                        -
                          -
                        • retry and retryBind: A function that allows you to retry a promise a certain number of times before giving up.
                        • -
                        • prettyMS: A function that converts milliseconds to a human-readable format.
                        • -
                        • loadFile: A function that loads a file from the file system, logging the process.
                        • -
                        • findNodeModule: A function that finds a node module in the file system.
                        • -
                        • escapeRegExp: A function that escapes a string to be used in a regular expression.
                        • -
                        • coerce: function for data type coercion, specially useful for environment variables and configuration files.
                        • -
                        • camelCase: function for converting strings to camelCase.
                        • -
                        • cycle: function for managing circular references in objects.
                        • -
                        • formatEnv: functions for formatting environment variables.
                        • -
                        • mock: functions for mocking objects, specially useful for testing in Jest.
                        • -
                        -

                        To install the @mdf.js/utils module, you can use the following commands:

                        -
                          -
                        • NPM
                        • -
                        -
                        npm install @mdf.js/utils
                        -
                        - -
                          -
                        • Yarn
                        • -
                        -
                        yarn add @mdf.js/utils
                        -
                        - -

                        The retry and retryBind functions are used to retry a promise a certain number of times before giving up. The difference between both functions is that the retryBind can bind the context of the promise to a concrete object.

                        -

                        Both functions have similar signatures:

                        -
                          -
                        • retry: retry<T>(task: TaskAsPromise<T>, funcArgs: TaskArguments, options: RetryOptions): Promise<T>.
                        • -
                        • retryBind: retryBind<T, U>(task: TaskAsPromise<T>, bindTo: U, funcArgs: TaskArguments, options: RetryOptions): Promise<T>.
                        • -
                        -

                        The common parameters are between both functions are:

                        -
                          -
                        • task (TaskAsPromise<T>): The task to be executed. TaskAsPromise<T> is alias type for (...args: TaskArguments) => Promise<T>.
                        • -
                        • funcArgs (TaskArguments): The arguments to be passed to the task. TaskArguments is an alias type for any[].
                        • -
                        • options (RetryOptions): The options for the retry. RetryOptions is an interface with the following properties: -
                            -
                          • logger (LoggerFunction): The logger function used for logging retry attempts. LoggerFunction is an alias type for (error: Crash | Multi | Boom) => void. Crash, Multi, and Boom are errors defined in the @mdf.js/crash.
                          • -
                          • waitTime (number): The time to wait between retry attempts, in milliseconds. Default is 1000.
                          • -
                          • maxWaitTime (number): The maximum time to wait between retry attempts, in milliseconds. Default is 15000.
                          • -
                          • abortSignal (AbortSignal): The signal to be used to interrupt the retry process. Default is null.
                          • -
                          • attempts (number): The maximum number of retry attempts. Default is Number.MAX_SAFE_INTEGER.
                          • -
                          • timeout (number): Timeout for each try. Default is undefined.
                          • -
                          • interrupt (() => boolean): A function that determines whether to interrupt the retry process. Should return true to interrupt, false otherwise. Default is undefined. Deprecated. Use abortSignal instead.
                          • -
                          -
                        • -
                        -

                        The extra parameter for the retryBind function is:

                        -
                          -
                        • bindTo (U): The object to bind the context of the promise to.
                        • -
                        -

                        Simple task:

                        -
                        import { retry, retryBind } from '@mdf.js/utils';
                        import { Logger } from '@mdf.js/logger';

                        const logger: Logger = new Logger();

                        const task = async (a: number, b: number) => {
                        if (Math.random() < 0.5) {
                        throw new Error('Random error');
                        }
                        return a + b;
                        };

                        const funcArgs = [1, 2];
                        const options = {
                        logger: logger.crash,
                        waitTime: 1000,
                        maxWaitTime: 15000,
                        attempts: 5,
                        timeout: 5000,
                        };

                        retry(task, [1, 2], options).then(console.log).catch(console.error); -
                        - -

                        Aborting the retry process can be done using an AbortSignal:

                        -
                        import { retry } from '@mdf.js/utils';
                        import { Logger } from '@mdf.js/logger';

                        const logger: Logger = new Logger();
                        const controller = new AbortController();

                        class MyContext {
                        public c = 10;
                        public task = async (a: number, b: number) => {
                        if (Math.random() < 0.5) {
                        throw new Error('Random error');
                        }
                        return a + b;
                        };
                        }

                        const context = new MyContext();

                        const options = {
                        logger: logger.crash,
                        waitTime: 1000,
                        maxWaitTime: 15000,
                        attempts: 5,
                        timeout: 5000,
                        abortSignal: controller.signal,
                        };

                        setTimeout(() => controller.abort(), 3000);
                        retryBind(context.task, context, [1, 2], options).then(console.log).catch(console.error); -
                        - -

                        The prettyMS function is used to convert milliseconds to a human-readable format. It has the following signature:

                        -
                          -
                        • prettyMS: (ms: number): string.
                        • -
                        -
                        import { prettyMS } from '@mdf.js/utils';

                        console.log(prettyMS(1000)); // 1s
                        console.log(prettyMS(1000 * 60)); // 1m
                        console.log(prettyMS(1000 * 60 * 60)); // 1h
                        console.log(prettyMS(1000 * 60 * 60 * 24)); // 1d -
                        - -

                        The loadFile function is used to load a file from the file system, logging the process. It has the following signature:

                        -
                          -
                        • loadFile: (path: string, logger?: LoggerInstance): Buffer | undefined. The logger parameter is optional and is used to log the process, it should be an instance of the LoggerInstance class from the @mdf.js/logger package or a simple object with a debug method that accepts a string.
                        • -
                        -
                        import { loadFile } from '@mdf.js/utils';

                        const logger = {
                        debug: (message: string) => console.log(message),
                        };
                        const file = loadFile('path/to/file', logger); -
                        - -

                        The findNodeModule function is used to find a node module in the file system. It has the following signature:

                        -
                          -
                        • findNodeModule: (module: string, dir?: string): string | undefined. The dir parameter is optional and is used to specify the current working directory, default is __dirname, this means that the search will start from the own module.
                        • -
                        -
                        import { findNodeModule } from '@mdf.js/utils';

                        const modulePath = findNodeModule('module-name'); -
                        - -

                        The escapeRegExp function is used to get the source of a regular expression pattern and escape it. It has the following signature:

                        -
                          -
                        • escapeRegExp: (regex: RexExp): string. The regex parameter is the regular expression to escape.
                        • -
                        -
                        import { escapeRegExp } from '@mdf.js/utils';

                        const escaped = escapeRegExp(/([.*+?^=!:${}()|\[\]\/\\])/g);
                        console.log(escaped); // \(\[\.\*\+\?\^\=\!\:\$\{\}\(\)\|\[\]\/\\]\) -
                        - -

                        The coerce function is used for data type coercion, specially useful for environment variables and configuration files. It has the following signature:

                        -
                          -
                        • coerce: <T extends Coerceable>(env: string | undefined, alternative?: T): T | undefined. The env parameter is the value to coerce, and the alternative parameter is the default value to return if the coercion fails. The Coerceable type is an alias type for number | boolean | Record<string, any> | any[] | null.
                        • -
                        -
                        import { coerce } from '@mdf.js/utils';

                        // process.env['MY_ENV_VAR'] = '1';
                        const asNumber = coerce<number>(process.env['MY_ENV_VAR'], 10); // Coerce to number, default to 10
                        // process.env['MY_ENV_VAR'] = 'true' or 'false';
                        const asBoolean = coerce<boolean>(process.env['MY_ENV_VAR'], true); // Coerce to boolean, default to true
                        // process.env['MY_ENV_VAR'] = '{"a": 1}';
                        const asObject = coerce<Record<string, any>>(process.env['MY_ENV_VAR'], { a: 1 }); // Coerce to object, default to { a: 1 }
                        // process.env['MY_ENV_VAR'] = '[1,2,3]';
                        const asArray = coerce<any[]>(process.env['MY_ENV_VAR'], [1, 2, 3]); // Coerce to array, default to [1, 2, 3]
                        // process.env['MY_ENV_VAR'] = 'null' or 'NULL';
                        const asNull = coerce(process.env['MY_ENV_VAR']); // Coerce to null -
                        - -

                        The camelCase function is used to convert strings to camelCase. It has the following signature:

                        -
                          -
                        • camelCase: (input: string | string[], options?: Options): string. The input parameter is the string or array of strings to convert, and the options parameter is an object with the following properties: -
                            -
                          • pascalCase (boolean): Convert to PascalCase. foo-bar -> FooBar. Default is false.
                          • -
                          • preserveConsecutiveUppercase (boolean): Preserve consecutive uppercase characters. foo-BAR -> fooBAR. Default is false.
                          • -
                          • locale (string | string[]): The locale parameter indicates the locale to be used to convert to upper/lower case according to any locale-specific case mappings. If multiple locales are given in an array, the best available locale is used. Setting locale: false ignores the platform locale and uses the Unicode Default Case Conversion algorithm. Default: The host environment’s current locale.
                          • -
                          -
                        • -
                        -
                        import { camelCase } from 'camelCase';

                        camelCase('foo-bar');
                        //=> 'fooBar'
                        camelCase('foo-bar', { pascalCase: true });
                        //=> 'FooBar'
                        camelCase('foo-BAR', { preserveConsecutiveUppercase: true });
                        //=> 'fooBAR'
                        camelCase('lorem-ipsum', { locale: 'en-US' });
                        //=> 'loremIpsum'
                        camelCase('lorem-ipsum', { locale: 'tr-TR' });
                        //=> 'loremİpsum'
                        camelCase('lorem-ipsum', { locale: ['en-US', 'en-GB'] });
                        //=> 'loremIpsum'
                        camelCase('lorem-ipsum', { locale: ['tr', 'TR', 'tr-TR'] });
                        //=> 'loremİpsum' -
                        - -

                        The deCycle and retroCycle functions are used to manage circular references in objects. The deCycle function is used to remove circular references from an object, and the retroCycle function is used to restore circular references to an object. They have the following signatures:

                        -
                          -
                        • deCycle: (object: any, replacer?: (value: any) => any): any. The object parameter is the object to remove circular references from, and the replacer parameter is a function that replaces circular references with a placeholder. Default is undefined.
                        • -
                        • retroCycle: (obj: any): any. The obj parameter is the object to restore circular references to.
                        • -
                        -
                        import { deCycle, retroCycle } from '@mdf.js/utils';

                        const obj = { a: 1 };
                        obj.b = obj;

                        const deCycled = deCycle(obj);
                        console.log(deCycled); // { a: 1, b: '$' }

                        const retroCycled = retroCycle(deCycled);
                        console.log(retroCycled); // { a: 1, b: [Circular] } -
                        - -

                        The formatEnv function is used to read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options. It has the following signatures:

                        -
                          -
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(): T. Read environment variables (process.env) and return an object with the values sanitized and the keys formatted.
                        • -
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string): T. Read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted.
                        • -
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string, options: Partial<ReadEnvOptions>): T. Read environment variables (process.env), filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options. The ReadEnvOptions type is an interface with the following properties: -
                            -
                          • separator (string): The separator to use for nested keys. Default is __.
                          • -
                          • format ('camelcase' | 'pascalcase' | 'lowercase' | 'uppercase'): The format to use for the keys. Default is 'camelcase'.
                          • -
                          • includePrefix (boolean): Whether to include the prefix in the keys. Default is false.
                          • -
                          -
                        • -
                        • formatEnv: <T extends Record<string, any> = Record<string, any>>(prefix: string, options: Partial<ReadEnvOptions>, source: Record<string, string | undefined>): T. Process a source, encoded as an environment variables file, filter them based on the indicated prefix, and return an object with the values sanitized and the keys formatted based on the specified options.
                        • -
                        -
                        import { formatEnv } from '@mdf.js/utils';

                        process.env['MY_OWN_TEST'] = 'test';

                        const env = {
                        EXAMPLE_OBJECT: '{"prop": "value"}',
                        EXAMPLE_ARRAY: '[1,2,3, "string", {"prop": "value"}, 5.2]',
                        EXAMPLE_INVALID_OBJECT: '{"prop": }"value"}',
                        EXAMPLE_INVALID_ARRAY: '[1,2,3, "string", ]{"prop": "value"}, 5.2]',
                        EXAMPLE_TRUE: 'true',
                        EXAMPLE_FALSE: 'false',
                        EXAMPLE_INT: '5',
                        EXAMPLE_NEGATIVE_INT: '-11',
                        EXAMPLE_FLOAT: '5.2456',
                        EXAMPLE_NEGATIVE_FLOAT: '-2.4567',
                        EXAMPLE_INT_ZERO: '0',
                        EXAMPLE_FLOAT_ZERO: '0.00',
                        EXAMPLE_NEGATIVE_INT_ZERO: '-0',
                        EXAMPLE_NEGATIVE_FLOAT_ZERO: '-0.00',
                        EXAMPLE_STRING: 'example',
                        EXAMPLE_DEEP__OBJECT__PROPERTY: 'value',
                        EXAMPLE_NOT_SHOULD_BE_SANITIZED: 5,
                        };

                        console.log(formatEnv()); // { myOwnTest: 'test' }

                        console.log(
                        formatEnv('EXAMPLE', { separator: '__', format: 'camelcase', includePrefix: false }, env)
                        );
                        // {
                        // object: { prop: 'value' },
                        // array: [1, 2, 3, 'string', { prop: 'value' }, 5.2],
                        // invalidObject: '{"prop": }"value"}',
                        // invalidArray: '[1,2,3, "string", ]{"prop": "value"}, 5.2]',
                        // true: true,
                        // false: false,
                        // int: 5,
                        // negativeInt: -11,
                        // float: 5.2456,
                        // negativeFloat: -2.4567,
                        // intZero: 0,
                        // floatZero: 0,
                        // negativeIntZero: -0,
                        // negativeFloatZero: -0,
                        // string: 'example',
                        // deep: {
                        // object: {
                        // property: 'value',
                        // },
                        // },
                        // notShouldBeSanitized: 5,
                        // } -
                        - -

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        -

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.

                        -
                        
                        -
                        - -

                        Index

                        Interfaces

                        Type Aliases

                        Variables

                        Functions

                        diff --git a/docs/types/_mdf.js_amqp-provider.Receiver.Provider.html b/docs/types/_mdf.js_amqp-provider.Receiver.Provider.html new file mode 100644 index 00000000..143c2adc --- /dev/null +++ b/docs/types/_mdf.js_amqp-provider.Receiver.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        Provider: Manager<Receiver, Config, Receiver.Port>
                        diff --git a/docs/types/_mdf.js_amqp-provider.Sender.Provider.html b/docs/types/_mdf.js_amqp-provider.Sender.Provider.html new file mode 100644 index 00000000..34521d36 --- /dev/null +++ b/docs/types/_mdf.js_amqp-provider.Sender.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        Provider: Manager<AwaitableSender, Config, Sender.Port>
                        diff --git a/docs/types/_mdf.js_core.Health.CheckEntry.html b/docs/types/_mdf.js_core.Health.CheckEntry.html new file mode 100644 index 00000000..45646759 --- /dev/null +++ b/docs/types/_mdf.js_core.Health.CheckEntry.html @@ -0,0 +1,2 @@ +CheckEntry | @mdf.js
                        CheckEntry: `${ComponentName}:${MeasurementName}`

                        A check entry is a string that represents a unique key in the checks object

                        +
                        diff --git a/docs/types/_mdf.js_core.Health.Checks.html b/docs/types/_mdf.js_core.Health.Checks.html new file mode 100644 index 00000000..e2d0afcc --- /dev/null +++ b/docs/types/_mdf.js_core.Health.Checks.html @@ -0,0 +1,31 @@ +Checks | @mdf.js
                        Checks: { [entry in CheckEntry]: Check<T>[] }

                        The “checks” object MAY have a number of unique keys, one for each logical sub-components. +Since each sub-component may be backed by several nodes with varying health statuses, the key +points to an array of objects. In case of a single-node sub-component (or if presence of nodes +is not relevant), a single-element array should be used as the value, for consistency. +The key identifying an element in the object should be a unique string within the details +section. It MAY have two parts: {componentName}:{metricName}, in which case the meaning of +the parts SHOULD be as follows:

                        +
                          +
                        • componentName: Human-readable name for the component. MUST not contain a colon, in the name, +since colon is used as a separator
                        • +
                        • metricName: Name of the metrics that the status is reported for. MUST not contain a colon, +in the name, since colon is used as a separator and can be one of: +
                            +
                          • Pre-defined value from this spec. Pre-defined values include: +
                              +
                            • utilization
                            • +
                            • responseTime
                            • +
                            • connections
                            • +
                            • uptime
                            • +
                            +
                          • +
                          • A common and standard term from a well-known source such as schema.org, IANA or +microformats.
                          • +
                          • A URI that indicates extra semantics and processing rules that MAY be provided by a +resource at the other end of the URI. URIs do not have to be dereferenceable, however. +They are just a namespace, and the meaning of a namespace CAN be provided by any +convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book).
                          • +
                          +
                        • +
                        +

                        Type Parameters

                        • T = any
                        diff --git a/docs/types/_mdf.js_core.Health.ComponentName.html b/docs/types/_mdf.js_core.Health.ComponentName.html new file mode 100644 index 00000000..3b27bc2d --- /dev/null +++ b/docs/types/_mdf.js_core.Health.ComponentName.html @@ -0,0 +1,2 @@ +ComponentName | @mdf.js
                        ComponentName: string

                        String alias type for a component name

                        +
                        diff --git a/docs/types/_mdf.js_core.Health.MeasurementName.html b/docs/types/_mdf.js_core.Health.MeasurementName.html new file mode 100644 index 00000000..d5d89530 --- /dev/null +++ b/docs/types/_mdf.js_core.Health.MeasurementName.html @@ -0,0 +1,2 @@ +MeasurementName | @mdf.js
                        MeasurementName: string

                        String alias type for a measurement name

                        +
                        diff --git a/docs/types/_mdf.js_core.Health.Status-1.html b/docs/types/_mdf.js_core.Health.Status-1.html new file mode 100644 index 00000000..4564b085 --- /dev/null +++ b/docs/types/_mdf.js_core.Health.Status-1.html @@ -0,0 +1,2 @@ +Status | @mdf.js
                        Status: typeof STATUSES[number]

                        Service status

                        +
                        diff --git a/docs/types/_mdf.js_core.Jobs.AnyHeaders.html b/docs/types/_mdf.js_core.Jobs.AnyHeaders.html new file mode 100644 index 00000000..68f3cb3a --- /dev/null +++ b/docs/types/_mdf.js_core.Jobs.AnyHeaders.html @@ -0,0 +1,2 @@ +AnyHeaders | @mdf.js
                        AnyHeaders: Record<string, any>

                        Any other extra header information

                        +
                        diff --git a/docs/types/_mdf.js_core.Jobs.AnyOptions.html b/docs/types/_mdf.js_core.Jobs.AnyOptions.html new file mode 100644 index 00000000..5db2ef30 --- /dev/null +++ b/docs/types/_mdf.js_core.Jobs.AnyOptions.html @@ -0,0 +1,2 @@ +AnyOptions | @mdf.js
                        AnyOptions: Record<string, any>

                        Any other extra option

                        +
                        diff --git a/docs/types/_mdf.js_core.Jobs.DoneEventHandler.html b/docs/types/_mdf.js_core.Jobs.DoneEventHandler.html new file mode 100644 index 00000000..b78d76e6 --- /dev/null +++ b/docs/types/_mdf.js_core.Jobs.DoneEventHandler.html @@ -0,0 +1,7 @@ +DoneEventHandler | @mdf.js

                        Type Alias DoneEventHandler<Type>

                        DoneEventHandler: (uuid: string, result: Result<Type>, error?: Multi) => void

                        Event handler for the done event, emitted when a job has ended, either due to completion or +failure.

                        +

                        Type Parameters

                        • Type extends string

                          The type of the job

                          +

                        Type declaration

                          • (uuid: string, result: Result<Type>, error?: Multi): void
                          • Parameters

                            • uuid: string

                              The unique identifier of the job

                              +
                            • result: Result<Type>

                              The result of the job

                              +
                            • Optionalerror: Multi

                              The error of the job, if any

                              +

                            Returns void

                        diff --git a/docs/types/_mdf.js_core.Jobs.Headers.html b/docs/types/_mdf.js_core.Jobs.Headers.html new file mode 100644 index 00000000..54fa7c9a --- /dev/null +++ b/docs/types/_mdf.js_core.Jobs.Headers.html @@ -0,0 +1,2 @@ +Headers | @mdf.js

                        Type Alias Headers<T>

                        Headers: T

                        Job headers

                        +

                        Type Parameters

                        diff --git a/docs/types/_mdf.js_core.Jobs.Options.html b/docs/types/_mdf.js_core.Jobs.Options.html new file mode 100644 index 00000000..fd8d236b --- /dev/null +++ b/docs/types/_mdf.js_core.Jobs.Options.html @@ -0,0 +1,2 @@ +Options | @mdf.js

                        Type Alias Options<CustomHeaders, CustomOptions>

                        Job options

                        +

                        Type Parameters

                        • CustomHeaders extends Record<string, any> = AnyHeaders
                        • CustomOptions extends Record<string, any> = AnyOptions
                        diff --git a/docs/types/_mdf.js_core.Layer.Observable.html b/docs/types/_mdf.js_core.Layer.Observable.html new file mode 100644 index 00000000..b819f849 --- /dev/null +++ b/docs/types/_mdf.js_core.Layer.Observable.html @@ -0,0 +1,2 @@ +Observable | @mdf.js
                        Observable:
                            | Manager<any, any, any>
                            | Layer.App.Component
                            | Resource
                            | Layer.App.Service

                        Represents an observable entity that can be monitored.

                        +
                        diff --git a/docs/types/_mdf.js_core.Layer.Provider.ProviderState.html b/docs/types/_mdf.js_core.Layer.Provider.ProviderState.html new file mode 100644 index 00000000..e83c143e --- /dev/null +++ b/docs/types/_mdf.js_core.Layer.Provider.ProviderState.html @@ -0,0 +1,2 @@ +ProviderState | @mdf.js
                        ProviderState: typeof PROVIDER_STATES[number]

                        Provider state type

                        +
                        diff --git a/docs/types/_mdf.js_crash.Cause.html b/docs/types/_mdf.js_crash.Cause.html new file mode 100644 index 00000000..c36d6ae9 --- /dev/null +++ b/docs/types/_mdf.js_crash.Cause.html @@ -0,0 +1,2 @@ +Cause | @mdf.js
                        Cause: Error | Crash | Multi

                        Error cause

                        +
                        diff --git a/docs/types/_mdf.js_crash.ContextLink.html b/docs/types/_mdf.js_crash.ContextLink.html new file mode 100644 index 00000000..3cf5581c --- /dev/null +++ b/docs/types/_mdf.js_crash.ContextLink.html @@ -0,0 +1,2 @@ +ContextLink | @mdf.js

                        Type Alias ContextLink

                        ContextLink: { [context: string]: string }

                        Context links, expressed as map of key-value pairs

                        +

                        Type declaration

                        • [context: string]: string
                        diff --git a/docs/types/_mdf.js_crash.Links.html b/docs/types/_mdf.js_crash.Links.html new file mode 100644 index 00000000..507471bd --- /dev/null +++ b/docs/types/_mdf.js_crash.Links.html @@ -0,0 +1,2 @@ +Links | @mdf.js
                        Links: { [link: string]: string | ContextLink }

                        Links that leads to further details about this particular occurrence of the problem.

                        +

                        Type declaration

                        diff --git a/docs/types/_mdf.js_crash.SimpleLink.html b/docs/types/_mdf.js_crash.SimpleLink.html new file mode 100644 index 00000000..e76da4df --- /dev/null +++ b/docs/types/_mdf.js_crash.SimpleLink.html @@ -0,0 +1,2 @@ +SimpleLink | @mdf.js

                        Type Alias SimpleLink

                        SimpleLink: string

                        Simple link expressed as a regular string

                        +
                        diff --git a/docs/types/_mdf.js_doorkeeper.ResultCallback.html b/docs/types/_mdf.js_doorkeeper.ResultCallback.html new file mode 100644 index 00000000..48f5966f --- /dev/null +++ b/docs/types/_mdf.js_doorkeeper.ResultCallback.html @@ -0,0 +1,2 @@ +ResultCallback | @mdf.js

                        Type Alias ResultCallback<T, K>

                        ResultCallback: (error?: Crash | Multi, result?: ValidatedOutput<T, K>) => void

                        Callback function for the validation process

                        +

                        Type Parameters

                        • T
                        • K

                        Type declaration

                        diff --git a/docs/types/_mdf.js_doorkeeper.SchemaSelector.html b/docs/types/_mdf.js_doorkeeper.SchemaSelector.html new file mode 100644 index 00000000..2373f780 --- /dev/null +++ b/docs/types/_mdf.js_doorkeeper.SchemaSelector.html @@ -0,0 +1 @@ +SchemaSelector | @mdf.js

                        Type Alias SchemaSelector<T>

                        SchemaSelector: T extends void ? string : keyof T & string

                        Type Parameters

                        • T
                        diff --git a/docs/types/_mdf.js_doorkeeper.ValidatedOutput.html b/docs/types/_mdf.js_doorkeeper.ValidatedOutput.html new file mode 100644 index 00000000..f9b349ac --- /dev/null +++ b/docs/types/_mdf.js_doorkeeper.ValidatedOutput.html @@ -0,0 +1 @@ +ValidatedOutput | @mdf.js

                        Type Alias ValidatedOutput<T, K>

                        ValidatedOutput: K extends keyof T ? T[K] : any

                        Type Parameters

                        • T
                        • K
                        diff --git a/docs/types/_mdf.js_elastic-provider.Elastic.Provider.html b/docs/types/_mdf.js_elastic-provider.Elastic.Provider.html new file mode 100644 index 00000000..2974ae82 --- /dev/null +++ b/docs/types/_mdf.js_elastic-provider.Elastic.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        Provider: Manager<Client, Config, Elastic.Port>
                        diff --git a/docs/types/_mdf.js_elastic-provider._internal_.Status.html b/docs/types/_mdf.js_elastic-provider._internal_.Status.html new file mode 100644 index 00000000..28ba45a0 --- /dev/null +++ b/docs/types/_mdf.js_elastic-provider._internal_.Status.html @@ -0,0 +1,16 @@ +Status | @mdf.js
                        Status: {
                            active_shards_percent: string;
                            cluster: string;
                            epoch: number;
                            init: number;
                            max_task_wait_time: number;
                            "node.data": number;
                            "node.total": number;
                            pending_tasks: number;
                            pri: number;
                            relo: number;
                            shards: number;
                            status: "red" | "yellow" | "green";
                            timestamp: string;
                            unassign: number;
                        }[]

                        Status

                        +

                        Type declaration

                        • active_shards_percent: string

                          Active number of shards in percent

                          +
                        • cluster: string

                          Cluster name

                          +
                        • epoch: number

                          Timestamp

                          +
                        • init: number

                          Number of initializing nodes

                          +
                        • max_task_wait_time: number

                          Wait time of longest task pending

                          +
                        • node.data: number

                          Number of nodes than can store data

                          +
                        • node.total: number

                          Total number of nodes

                          +
                        • pending_tasks: number

                          Number of pending tasks

                          +
                        • pri: number

                          Number of primary shards

                          +
                        • relo: number

                          Number of relocating nodes

                          +
                        • shards: number

                          Number of shards

                          +
                        • status: "red" | "yellow" | "green"

                          Health status

                          +
                        • timestamp: string

                          Timestamp

                          +
                        • unassign: number

                          Number of unassigned shards

                          +
                        diff --git a/docs/types/_mdf.js_faker.Builder.html b/docs/types/_mdf.js_faker.Builder.html new file mode 100644 index 00000000..f9ae07f0 --- /dev/null +++ b/docs/types/_mdf.js_faker.Builder.html @@ -0,0 +1,2 @@ +Builder | @mdf.js

                        Type Alias Builder<T, K>

                        Builder: (...args: any) => T[K] | undefined

                        Type for function for attribute builder function

                        +

                        Type Parameters

                        • T
                        • K extends keyof T

                        Type declaration

                          • (...args: any): T[K] | undefined
                          • Parameters

                            • ...args: any

                            Returns T[K] | undefined

                        diff --git a/docs/types/_mdf.js_faker.DefaultValue.html b/docs/types/_mdf.js_faker.DefaultValue.html new file mode 100644 index 00000000..3b3142e6 --- /dev/null +++ b/docs/types/_mdf.js_faker.DefaultValue.html @@ -0,0 +1,2 @@ +DefaultValue | @mdf.js

                        Type Alias DefaultValue<T, K>

                        DefaultValue: T[K]

                        Type for attribute default value

                        +

                        Type Parameters

                        • T
                        • K extends keyof T
                        diff --git a/docs/types/_mdf.js_faker.Dependencies.html b/docs/types/_mdf.js_faker.Dependencies.html new file mode 100644 index 00000000..f0fa71f9 --- /dev/null +++ b/docs/types/_mdf.js_faker.Dependencies.html @@ -0,0 +1,2 @@ +Dependencies | @mdf.js

                        Type Alias Dependencies<T, K>

                        Dependencies: (K | string)[]

                        Type for attribute dependencies

                        +

                        Type Parameters

                        • T
                        • K extends keyof T
                        diff --git a/docs/types/_mdf.js_faker._internal_.GeneratorOptions.html b/docs/types/_mdf.js_faker._internal_.GeneratorOptions.html new file mode 100644 index 00000000..5361bc7f --- /dev/null +++ b/docs/types/_mdf.js_faker._internal_.GeneratorOptions.html @@ -0,0 +1,2 @@ +GeneratorOptions | @mdf.js

                        Type Alias GeneratorOptions<T, K>

                        GeneratorOptions: Builder<T, K> | DefaultValue<T, K> | Dependencies<T, K>

                        Type for attribute value option

                        +

                        Type Parameters

                        • T
                        • K extends keyof T
                        diff --git a/docs/types/_mdf.js_file-flinger._internal_.InternalKeygenOptions.html b/docs/types/_mdf.js_file-flinger._internal_.InternalKeygenOptions.html new file mode 100644 index 00000000..99e1c6f5 --- /dev/null +++ b/docs/types/_mdf.js_file-flinger._internal_.InternalKeygenOptions.html @@ -0,0 +1,2 @@ +InternalKeygenOptions | @mdf.js
                        InternalKeygenOptions: Required<Omit<KeygenOptions, "filePattern">> & {
                            filePattern: string | undefined;
                        }

                        Internal key generator options

                        +
                        diff --git a/docs/types/_mdf.js_file-flinger._internal_.InternalWatcherOptions.html b/docs/types/_mdf.js_file-flinger._internal_.InternalWatcherOptions.html new file mode 100644 index 00000000..87288f5b --- /dev/null +++ b/docs/types/_mdf.js_file-flinger._internal_.InternalWatcherOptions.html @@ -0,0 +1,2 @@ +InternalWatcherOptions | @mdf.js
                        InternalWatcherOptions: Required<Omit<WatcherOptions, "cwd">> & {
                            cwd: string | undefined;
                        }

                        Internal watcher options

                        +
                        diff --git a/docs/types/_mdf.js_file-flinger._internal_.MetricInstances.html b/docs/types/_mdf.js_file-flinger._internal_.MetricInstances.html new file mode 100644 index 00000000..9b5fbcb0 --- /dev/null +++ b/docs/types/_mdf.js_file-flinger._internal_.MetricInstances.html @@ -0,0 +1,5 @@ +MetricInstances | @mdf.js
                        MetricInstances: {
                            jobsDuration: Histogram;
                            jobsProcessed: Counter;
                            jobsWithError: Counter;
                        }

                        Metric types

                        +

                        Type declaration

                        • jobsDuration: Histogram

                          File flinger jobs duration

                          +
                        • jobsProcessed: Counter

                          The total number of all jobs processed

                          +
                        • jobsWithError: Counter

                          The total number errors processing jobs

                          +
                        diff --git a/docs/types/_mdf.js_firehose.JobEventHandler.html b/docs/types/_mdf.js_firehose.JobEventHandler.html new file mode 100644 index 00000000..adff247d --- /dev/null +++ b/docs/types/_mdf.js_firehose.JobEventHandler.html @@ -0,0 +1,2 @@ +JobEventHandler | @mdf.js

                        Type Alias JobEventHandler<Type, Data, CustomHeaders, CustomOptions>

                        JobEventHandler: (
                            job: JobObject<Type, Data, CustomHeaders, CustomOptions>,
                        ) => void

                        Firehose job event handler

                        +

                        Type Parameters

                        • Type extends string = string
                        • Data = any
                        • CustomHeaders extends Record<string, any> = NoMoreHeaders
                        • CustomOptions extends Record<string, any> = NoMoreOptions

                        Type declaration

                        diff --git a/docs/types/_mdf.js_firehose.Plugs.Sink.Any.html b/docs/types/_mdf.js_firehose.Plugs.Sink.Any.html new file mode 100644 index 00000000..4367038e --- /dev/null +++ b/docs/types/_mdf.js_firehose.Plugs.Sink.Any.html @@ -0,0 +1 @@ +Any | @mdf.js

                        Type Alias Any<Type, Data, CustomHeaders, CustomOptions>

                        Type Parameters

                        • Type extends string = string
                        • Data = any
                        • CustomHeaders extends Record<string, any> = AnyHeaders
                        • CustomOptions extends Record<string, any> = AnyOptions
                        diff --git a/docs/types/_mdf.js_firehose.Plugs.Source.Any.html b/docs/types/_mdf.js_firehose.Plugs.Source.Any.html new file mode 100644 index 00000000..78384d8e --- /dev/null +++ b/docs/types/_mdf.js_firehose.Plugs.Source.Any.html @@ -0,0 +1 @@ +Any | @mdf.js

                        Type Alias Any<Type, Data, CustomHeaders, CustomOptions>

                        Type Parameters

                        • Type extends string = string
                        • Data = any
                        • CustomHeaders extends Record<string, any> = NoMoreHeaders
                        • CustomOptions extends Record<string, any> = NoMoreOptions
                        diff --git a/docs/types/_mdf.js_firehose._internal_.MetricInstances.html b/docs/types/_mdf.js_firehose._internal_.MetricInstances.html new file mode 100644 index 00000000..9ee11f16 --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.MetricInstances.html @@ -0,0 +1,7 @@ +MetricInstances | @mdf.js
                        MetricInstances: {
                            jobsDuration: Histogram;
                            jobsInProcess: Gauge;
                            jobsProcessed: Counter;
                            jobsThroughput: Histogram;
                            jobsWithError: Counter;
                        }

                        Metric types

                        +

                        Type declaration

                        • jobsDuration: Histogram

                          Firehose jobs duration

                          +
                        • jobsInProcess: Gauge

                          Number of jobs actually processing

                          +
                        • jobsProcessed: Counter

                          The total number of all jobs processed

                          +
                        • jobsThroughput: Histogram

                          Firehose throughput in bytes

                          +
                        • jobsWithError: Counter

                          The total number errors processing jobs

                          +
                        diff --git a/docs/types/_mdf.js_firehose._internal_.OpenJobHandler.html b/docs/types/_mdf.js_firehose._internal_.OpenJobHandler.html new file mode 100644 index 00000000..6ac7c3e7 --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.OpenJobHandler.html @@ -0,0 +1,2 @@ +OpenJobHandler | @mdf.js
                        OpenJobHandler: JobHandler<any, any, any, any>

                        Auxiliary type to define the open job handler

                        +
                        diff --git a/docs/types/_mdf.js_firehose._internal_.OpenJobObject.html b/docs/types/_mdf.js_firehose._internal_.OpenJobObject.html new file mode 100644 index 00000000..34b7acca --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.OpenJobObject.html @@ -0,0 +1,2 @@ +OpenJobObject | @mdf.js
                        OpenJobObject: JobObject<any, any, any, any>

                        Auxiliary type to define the open job object

                        +
                        diff --git a/docs/types/_mdf.js_firehose._internal_.OpenJobRequest.html b/docs/types/_mdf.js_firehose._internal_.OpenJobRequest.html new file mode 100644 index 00000000..d5302a4d --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.OpenJobRequest.html @@ -0,0 +1,2 @@ +OpenJobRequest | @mdf.js
                        OpenJobRequest: JobRequest<any, any, any, any>

                        Auxiliary type to define the open job request

                        +
                        diff --git a/docs/types/_mdf_js_openc2_core.Control.AllowedResultPropertyTypes.html b/docs/types/_mdf.js_firehose._internal_.OpenStrategy.html similarity index 50% rename from docs/types/_mdf_js_openc2_core.Control.AllowedResultPropertyTypes.html rename to docs/types/_mdf.js_firehose._internal_.OpenStrategy.html index 79499316..0f875c5a 100644 --- a/docs/types/_mdf_js_openc2_core.Control.AllowedResultPropertyTypes.html +++ b/docs/types/_mdf.js_firehose._internal_.OpenStrategy.html @@ -1,2 +1,2 @@ -AllowedResultPropertyTypes | @mdf.js
                        AllowedResultPropertyTypes:
                            | Record<string, any>
                            | string[]
                            | number
                            | string
                            | ActionTargetPairs
                            | undefined

                        Allowed property types

                        -
                        +OpenStrategy | @mdf.js
                        OpenStrategy: Jobs.Strategy<any, any, any, any>

                        Auxiliary type to define the open strategy

                        +
                        diff --git a/docs/types/_mdf.js_firehose._internal_.Sinks.html b/docs/types/_mdf.js_firehose._internal_.Sinks.html new file mode 100644 index 00000000..b4794cff --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.Sinks.html @@ -0,0 +1 @@ +Sinks | @mdf.js
                        diff --git a/docs/types/_mdf.js_firehose._internal_.Sources.html b/docs/types/_mdf.js_firehose._internal_.Sources.html new file mode 100644 index 00000000..36e4885e --- /dev/null +++ b/docs/types/_mdf.js_firehose._internal_.Sources.html @@ -0,0 +1 @@ +Sources | @mdf.js
                        diff --git a/docs/types/_mdf.js_http-client-provider.HTTP.Provider.html b/docs/types/_mdf.js_http-client-provider.HTTP.Provider.html new file mode 100644 index 00000000..cd06d186 --- /dev/null +++ b/docs/types/_mdf.js_http-client-provider.HTTP.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_http-server-provider.HTTP.Provider.html b/docs/types/_mdf.js_http-server-provider.HTTP.Provider.html new file mode 100644 index 00000000..90605c5a --- /dev/null +++ b/docs/types/_mdf.js_http-server-provider.HTTP.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Config.html b/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Config.html new file mode 100644 index 00000000..c4480bfe --- /dev/null +++ b/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Config.html @@ -0,0 +1 @@ +Config | @mdf.js
                        diff --git a/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Provider.html b/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Provider.html new file mode 100644 index 00000000..9b9472c9 --- /dev/null +++ b/docs/types/_mdf.js_jsonl-archiver.JSONLArchiver.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_kafka-provider.Consumer.Provider.html b/docs/types/_mdf.js_kafka-provider.Consumer.Provider.html new file mode 100644 index 00000000..06bc3e39 --- /dev/null +++ b/docs/types/_mdf.js_kafka-provider.Consumer.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_kafka-provider.Producer.Provider.html b/docs/types/_mdf.js_kafka-provider.Producer.Provider.html new file mode 100644 index 00000000..6d145ba2 --- /dev/null +++ b/docs/types/_mdf.js_kafka-provider.Producer.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_kafka-provider._internal_.SystemStatus.html b/docs/types/_mdf.js_kafka-provider._internal_.SystemStatus.html new file mode 100644 index 00000000..69a549ef --- /dev/null +++ b/docs/types/_mdf.js_kafka-provider._internal_.SystemStatus.html @@ -0,0 +1 @@ +SystemStatus | @mdf.js
                        SystemStatus: { groups: GroupDescription[]; topics: ITopicMetadata[] }

                        Type declaration

                        • groups: GroupDescription[]
                        • topics: ITopicMetadata[]
                        diff --git a/docs/types/_mdf.js_logger.FluentdTransportConfig.html b/docs/types/_mdf.js_logger.FluentdTransportConfig.html new file mode 100644 index 00000000..17a08d36 --- /dev/null +++ b/docs/types/_mdf.js_logger.FluentdTransportConfig.html @@ -0,0 +1,4 @@ +FluentdTransportConfig | @mdf.js

                        Type Alias FluentdTransportConfig

                        FluentdTransportConfig: Exclude<Options, "internalLogger"> & {
                            enabled?: boolean;
                            level?: LogLevel;
                        }

                        Fluentd transport configuration

                        +

                        Type declaration

                        • Optionalenabled?: boolean

                          Fluentd transport enabled, default: false

                          +
                        • Optionallevel?: LogLevel

                          Fluentd log level, default: info

                          +
                        diff --git a/docs/types/_mdf.js_logger.LogLevel.html b/docs/types/_mdf.js_logger.LogLevel.html new file mode 100644 index 00000000..c3f508ce --- /dev/null +++ b/docs/types/_mdf.js_logger.LogLevel.html @@ -0,0 +1 @@ +LogLevel | @mdf.js
                        LogLevel: typeof LOG_LEVELS[number]
                        diff --git a/docs/types/_mdf.js_logger.LoggerFunction.html b/docs/types/_mdf.js_logger.LoggerFunction.html new file mode 100644 index 00000000..b3425dfb --- /dev/null +++ b/docs/types/_mdf.js_logger.LoggerFunction.html @@ -0,0 +1 @@ +LoggerFunction | @mdf.js

                        Type Alias LoggerFunction

                        LoggerFunction: (
                            message: string,
                            uuid?: string,
                            context?: string,
                            ...meta: any[],
                        ) => void

                        Type declaration

                          • (message: string, uuid?: string, context?: string, ...meta: any[]): void
                          • Parameters

                            • message: string
                            • Optionaluuid: string
                            • Optionalcontext: string
                            • ...meta: any[]

                            Returns void

                        diff --git a/docs/types/_mdf.js_middlewares.AfterRoutesMiddlewares.html b/docs/types/_mdf.js_middlewares.AfterRoutesMiddlewares.html new file mode 100644 index 00000000..34fa613a --- /dev/null +++ b/docs/types/_mdf.js_middlewares.AfterRoutesMiddlewares.html @@ -0,0 +1 @@ +AfterRoutesMiddlewares | @mdf.js

                        Type Alias AfterRoutesMiddlewares

                        AfterRoutesMiddlewares: ErrorHandler | Default
                        diff --git a/docs/types/_mdf.js_middlewares.AuditCategory.html b/docs/types/_mdf.js_middlewares.AuditCategory.html new file mode 100644 index 00000000..d5dc913a --- /dev/null +++ b/docs/types/_mdf.js_middlewares.AuditCategory.html @@ -0,0 +1,4 @@ +AuditCategory | @mdf.js
                        AuditCategory: "create" | "modify" | "execute" | "access" | "delete"

                        Copyright 2024 Mytra Control S.L. All rights reserved.

                        +

                        Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                        +
                        diff --git a/docs/types/_mdf.js_middlewares.AuthZOptions.html b/docs/types/_mdf.js_middlewares.AuthZOptions.html new file mode 100644 index 00000000..3d650688 --- /dev/null +++ b/docs/types/_mdf.js_middlewares.AuthZOptions.html @@ -0,0 +1,6 @@ +AuthZOptions | @mdf.js
                        AuthZOptions: {
                            algorithms?: Algorithm[];
                            onAuthorization?: (decodedToken: JwtPayload) => {};
                            role?: string | string[];
                            secret?: string;
                        }

                        Options for configuring authorization middleware

                        +

                        Type declaration

                        • Optionalalgorithms?: Algorithm[]

                          The algorithms used for JWT token verification

                          +
                        • OptionalonAuthorization?: (decodedToken: JwtPayload) => {}

                          A callback function called when authorization is successful.

                          +
                        • Optionalrole?: string | string[]

                          The role(s) required for accessing the protected route

                          +
                        • Optionalsecret?: string

                          The secret used to sign and verify JWT tokens

                          +
                        diff --git a/docs/types/_mdf.js_middlewares.BeforeRoutesMiddlewares.html b/docs/types/_mdf.js_middlewares.BeforeRoutesMiddlewares.html new file mode 100644 index 00000000..7ba34781 --- /dev/null +++ b/docs/types/_mdf.js_middlewares.BeforeRoutesMiddlewares.html @@ -0,0 +1 @@ +BeforeRoutesMiddlewares | @mdf.js

                        Type Alias BeforeRoutesMiddlewares

                        BeforeRoutesMiddlewares:
                            | Audit
                            | AuthZ
                            | BodyParser
                            | Cache
                            | Cors
                            | Security
                            | Logger
                            | Metrics
                            | Multer
                            | NoCache
                            | RateLimiter
                            | RequestId
                        diff --git a/docs/types/_mdf.js_middlewares.EndpointsMiddlewares.html b/docs/types/_mdf.js_middlewares.EndpointsMiddlewares.html new file mode 100644 index 00000000..964f2b18 --- /dev/null +++ b/docs/types/_mdf.js_middlewares.EndpointsMiddlewares.html @@ -0,0 +1 @@ +EndpointsMiddlewares | @mdf.js

                        Type Alias EndpointsMiddlewares

                        EndpointsMiddlewares: Audit | AuthZ | Cache | NoCache | RateLimiter
                        diff --git a/docs/types/_mdf.js_middlewares.Middlewares.html b/docs/types/_mdf.js_middlewares.Middlewares.html new file mode 100644 index 00000000..2ddcb1f6 --- /dev/null +++ b/docs/types/_mdf.js_middlewares.Middlewares.html @@ -0,0 +1 @@ +Middlewares | @mdf.js
                        Middlewares:
                            | Audit
                            | AuthZ
                            | BodyParser
                            | Cache
                            | Cors
                            | Default
                            | ErrorHandler
                            | Security
                            | Logger
                            | Metrics
                            | Multer
                            | NoCache
                            | RateLimiter
                            | RequestId
                        diff --git a/docs/types/_mdf.js_middlewares._internal_.CacheEntry.html b/docs/types/_mdf.js_middlewares._internal_.CacheEntry.html new file mode 100644 index 00000000..8820f412 --- /dev/null +++ b/docs/types/_mdf.js_middlewares._internal_.CacheEntry.html @@ -0,0 +1,6 @@ +CacheEntry | @mdf.js
                        CacheEntry: {
                            body: any;
                            date: number;
                            duration: number;
                            headers: { [x: string]: OutgoingHttpHeaderValue };
                            status: number;
                        }

                        Type declaration

                        • body: any

                          Response body

                          +
                        • date: number

                          Cache entry date

                          +
                        • duration: number

                          Cache entry duration

                          +
                        • headers: { [x: string]: OutgoingHttpHeaderValue }

                          Response headers

                          +
                        • status: number

                          Status of response

                          +
                        diff --git a/docs/types/_mdf.js_middlewares._internal_.MetricInstances.html b/docs/types/_mdf.js_middlewares._internal_.MetricInstances.html new file mode 100644 index 00000000..2768f42a --- /dev/null +++ b/docs/types/_mdf.js_middlewares._internal_.MetricInstances.html @@ -0,0 +1,14 @@ +MetricInstances | @mdf.js
                        MetricInstances: {
                            api_all_client_error_total: Counter;
                            api_all_errors_total: Counter;
                            api_all_info_total: Counter;
                            api_all_redirect_total: Counter;
                            api_all_request_in_processing_total: Gauge;
                            api_all_request_total: Counter;
                            api_all_server_error_total: Counter;
                            api_all_success_total: Counter;
                            api_request_duration_milliseconds: Histogram;
                            api_request_size_bytes: Histogram;
                            api_request_total: Counter;
                            api_response_size_bytes: Histogram;
                        }

                        Metric types

                        +

                        Type declaration

                        • api_all_client_error_total: Counter

                          The total number of all API requests with client error response

                          +
                        • api_all_errors_total: Counter

                          The total number of all API requests with error response

                          +
                        • api_all_info_total: Counter

                          The total number of all API requests with informative response

                          +
                        • api_all_redirect_total: Counter

                          The total number of all API requests with redirect response

                          +
                        • api_all_request_in_processing_total: Gauge

                          The total number of all API requests currently in processing (no response yet)

                          +
                        • api_all_request_total: Counter

                          The total number of all API requests received

                          +
                        • api_all_server_error_total: Counter

                          The total number of all API requests with server error response

                          +
                        • api_all_success_total: Counter

                          The total number of all API requests with success response

                          +
                        • api_request_duration_milliseconds: Histogram

                          API requests duration

                          +
                        • api_request_size_bytes: Histogram

                          API requests size

                          +
                        • api_request_total: Counter

                          The total number of all API requests

                          +
                        • api_response_size_bytes: Histogram

                          API requests size

                          +
                        diff --git a/docs/types/_mdf.js_middlewares._internal_.OutgoingHttpHeaderValue.html b/docs/types/_mdf.js_middlewares._internal_.OutgoingHttpHeaderValue.html new file mode 100644 index 00000000..794f4e6b --- /dev/null +++ b/docs/types/_mdf.js_middlewares._internal_.OutgoingHttpHeaderValue.html @@ -0,0 +1,2 @@ +OutgoingHttpHeaderValue | @mdf.js
                        OutgoingHttpHeaderValue: number | string | string[] | undefined

                        Possible type of values for outgoing headers

                        +
                        diff --git a/docs/types/_mdf.js_mongo-provider.Mongo.Provider.html b/docs/types/_mdf.js_mongo-provider.Mongo.Provider.html new file mode 100644 index 00000000..e4f9b008 --- /dev/null +++ b/docs/types/_mdf.js_mongo-provider.Mongo.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_mqtt-provider.MQTT.Provider.html b/docs/types/_mdf.js_mqtt-provider.MQTT.Provider.html new file mode 100644 index 00000000..91b99430 --- /dev/null +++ b/docs/types/_mdf.js_mqtt-provider.MQTT.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                        diff --git a/docs/types/_mdf.js_openc2-core.CommandJobDone.html b/docs/types/_mdf.js_openc2-core.CommandJobDone.html new file mode 100644 index 00000000..3266ecf0 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.CommandJobDone.html @@ -0,0 +1 @@ +CommandJobDone | @mdf.js
                        CommandJobDone: Result<"command"> & { command: CommandMessage }
                        diff --git a/docs/types/_mdf.js_openc2-core.CommandJobHandler.html b/docs/types/_mdf.js_openc2-core.CommandJobHandler.html new file mode 100644 index 00000000..95c732ff --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.CommandJobHandler.html @@ -0,0 +1 @@ +CommandJobHandler | @mdf.js
                        CommandJobHandler: JobHandler<"command", CommandMessage, CommandJobHeader>
                        diff --git a/docs/types/_mdf.js_openc2-core.Control.ActionTargetPairs.html b/docs/types/_mdf.js_openc2-core.Control.ActionTargetPairs.html new file mode 100644 index 00000000..3bdd6496 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Control.ActionTargetPairs.html @@ -0,0 +1,2 @@ +ActionTargetPairs | @mdf.js
                        ActionTargetPairs: { [Property in ActionType]?: string[] }

                        Map of each action supported by this actuator to the list of targets applicable to that action

                        +
                        diff --git a/docs/types/_mdf.js_openc2-core.Control.ActionType.html b/docs/types/_mdf.js_openc2-core.Control.ActionType.html new file mode 100644 index 00000000..ea192b99 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Control.ActionType.html @@ -0,0 +1 @@ +ActionType | @mdf.js
                        ActionType: `${Action}`
                        diff --git a/docs/types/_mdf.js_openc2-core.Control.AllowedResultPropertyTypes.html b/docs/types/_mdf.js_openc2-core.Control.AllowedResultPropertyTypes.html new file mode 100644 index 00000000..32971be4 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Control.AllowedResultPropertyTypes.html @@ -0,0 +1,2 @@ +AllowedResultPropertyTypes | @mdf.js
                        AllowedResultPropertyTypes:
                            | Record<string, any>
                            | string[]
                            | number
                            | string
                            | ActionTargetPairs
                            | undefined

                        Allowed property types

                        +
                        diff --git a/docs/types/_mdf.js_openc2-core.Control.Message.html b/docs/types/_mdf.js_openc2-core.Control.Message.html new file mode 100644 index 00000000..ff0c809f --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Control.Message.html @@ -0,0 +1 @@ +Message | @mdf.js
                        diff --git a/docs/types/_mdf.js_openc2-core.Control.Namespace.html b/docs/types/_mdf.js_openc2-core.Control.Namespace.html new file mode 100644 index 00000000..3bed433e --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Control.Namespace.html @@ -0,0 +1,2 @@ +Namespace | @mdf.js
                        Namespace: `x-${string}`

                        Namespace type

                        +
                        diff --git a/docs/types/_mdf.js_openc2-core.OnCommandHandler.html b/docs/types/_mdf.js_openc2-core.OnCommandHandler.html new file mode 100644 index 00000000..ab20e9a4 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.OnCommandHandler.html @@ -0,0 +1 @@ +OnCommandHandler | @mdf.js
                        OnCommandHandler: (
                            message: CommandMessage,
                            done: (error?: Crash | Error, message?: ResponseMessage) => void,
                        ) => void

                        Type declaration

                        diff --git a/docs/types/_mdf.js_openc2-core.Resolver.html b/docs/types/_mdf.js_openc2-core.Resolver.html new file mode 100644 index 00000000..714eb23a --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.Resolver.html @@ -0,0 +1 @@ +Resolver | @mdf.js
                        Resolver: <T = any>(target: T) => Promise<AllowedResultPropertyTypes | void>

                        Type declaration

                        diff --git a/docs/types/_mdf.js_openc2-core.ResolverEntry.html b/docs/types/_mdf.js_openc2-core.ResolverEntry.html new file mode 100644 index 00000000..92f2c98b --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.ResolverEntry.html @@ -0,0 +1 @@ +ResolverEntry | @mdf.js
                        ResolverEntry: `${ActionType}:${Namespace}:${string}`
                        diff --git a/docs/types/_mdf.js_openc2-core.ResolverMap.html b/docs/types/_mdf.js_openc2-core.ResolverMap.html new file mode 100644 index 00000000..13c679e9 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core.ResolverMap.html @@ -0,0 +1 @@ +ResolverMap | @mdf.js
                        ResolverMap: { [entry in ResolverEntry]?: Resolver }
                        diff --git a/docs/types/_mdf.js_openc2-core._internal_.CommandResponse.html b/docs/types/_mdf.js_openc2-core._internal_.CommandResponse.html new file mode 100644 index 00000000..f518d008 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core._internal_.CommandResponse.html @@ -0,0 +1 @@ +CommandResponse | @mdf.js
                        diff --git a/docs/types/_mdf.js_openc2-core._internal_.CommandResponseHandler.html b/docs/types/_mdf.js_openc2-core._internal_.CommandResponseHandler.html new file mode 100644 index 00000000..959dcfb6 --- /dev/null +++ b/docs/types/_mdf.js_openc2-core._internal_.CommandResponseHandler.html @@ -0,0 +1 @@ +CommandResponseHandler | @mdf.js
                        CommandResponseHandler: (
                            error?: Crash | Error,
                            message?: CommandResponse,
                        ) => void

                        Type declaration

                        diff --git a/docs/types/_mdf.js_openc2.Adapters.Dummy.Config.html b/docs/types/_mdf.js_openc2.Adapters.Dummy.Config.html new file mode 100644 index 00000000..5796df8d --- /dev/null +++ b/docs/types/_mdf.js_openc2.Adapters.Dummy.Config.html @@ -0,0 +1 @@ +Config | @mdf.js
                        Config: {}

                        Type declaration

                          diff --git a/docs/types/_mdf.js_openc2.Adapters.Redis.Config.html b/docs/types/_mdf.js_openc2.Adapters.Redis.Config.html new file mode 100644 index 00000000..3eb301b4 --- /dev/null +++ b/docs/types/_mdf.js_openc2.Adapters.Redis.Config.html @@ -0,0 +1 @@ +Config | @mdf.js
                          diff --git a/docs/types/_mdf.js_openc2.Adapters.SocketIO.Config.html b/docs/types/_mdf.js_openc2.Adapters.SocketIO.Config.html new file mode 100644 index 00000000..23baa087 --- /dev/null +++ b/docs/types/_mdf.js_openc2.Adapters.SocketIO.Config.html @@ -0,0 +1 @@ +Config | @mdf.js
                          diff --git a/docs/types/_mdf.js_openc2.RedisClientOptions.html b/docs/types/_mdf.js_openc2.RedisClientOptions.html new file mode 100644 index 00000000..46665409 --- /dev/null +++ b/docs/types/_mdf.js_openc2.RedisClientOptions.html @@ -0,0 +1,2 @@ +RedisClientOptions | @mdf.js

                          Type Alias RedisClientOptions

                          RedisClientOptions: Redis.Config

                          Redis client options

                          +
                          diff --git a/docs/types/_mdf.js_openc2.SocketIOClientOptions.html b/docs/types/_mdf.js_openc2.SocketIOClientOptions.html new file mode 100644 index 00000000..b8d3ea8a --- /dev/null +++ b/docs/types/_mdf.js_openc2.SocketIOClientOptions.html @@ -0,0 +1,2 @@ +SocketIOClientOptions | @mdf.js

                          Type Alias SocketIOClientOptions

                          SocketIOClientOptions: SocketIOClient.Config

                          SocketIO client options

                          +
                          diff --git a/docs/types/_mdf.js_openc2.SocketIOServerOptions.html b/docs/types/_mdf.js_openc2.SocketIOServerOptions.html new file mode 100644 index 00000000..00be7a2c --- /dev/null +++ b/docs/types/_mdf.js_openc2.SocketIOServerOptions.html @@ -0,0 +1,2 @@ +SocketIOServerOptions | @mdf.js

                          Type Alias SocketIOServerOptions

                          SocketIOServerOptions: SocketIOServer.Config

                          SocketIO server options

                          +
                          diff --git a/docs/types/_mdf.js_redis-provider.Redis.Config.html b/docs/types/_mdf.js_redis-provider.Redis.Config.html new file mode 100644 index 00000000..1002c50f --- /dev/null +++ b/docs/types/_mdf.js_redis-provider.Redis.Config.html @@ -0,0 +1 @@ +Config | @mdf.js
                          Config: Omit<RedisOptions, "keepAlive"> & {
                              checkInterval?: number;
                              disableChecks?: boolean;
                              keepAlive?: number;
                          }
                          diff --git a/docs/types/_mdf.js_redis-provider.Redis.Provider.html b/docs/types/_mdf.js_redis-provider.Redis.Provider.html new file mode 100644 index 00000000..d71e74be --- /dev/null +++ b/docs/types/_mdf.js_redis-provider.Redis.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                          diff --git a/docs/types/_mdf.js_redis-provider._internal_.ProcessesSupervised.html b/docs/types/_mdf.js_redis-provider._internal_.ProcessesSupervised.html new file mode 100644 index 00000000..8f5567f2 --- /dev/null +++ b/docs/types/_mdf.js_redis-provider._internal_.ProcessesSupervised.html @@ -0,0 +1,4 @@ +ProcessesSupervised | @mdf.js
                          ProcessesSupervised: "upstart" | "systemd" | "unknown" | "no"

                          Copyright 2024 Mytra Control S.L. All rights reserved.

                          +

                          Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                          +
                          diff --git a/docs/types/_mdf.js_s3-provider.S3.Provider.html b/docs/types/_mdf.js_s3-provider.S3.Provider.html new file mode 100644 index 00000000..ebb4d74f --- /dev/null +++ b/docs/types/_mdf.js_s3-provider.S3.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                          Provider: Manager<Client, Config, S3.Port>
                          diff --git a/docs/types/_mdf.js_service-registry.ConsumerAdapterOptions.html b/docs/types/_mdf.js_service-registry.ConsumerAdapterOptions.html new file mode 100644 index 00000000..468dde87 --- /dev/null +++ b/docs/types/_mdf.js_service-registry.ConsumerAdapterOptions.html @@ -0,0 +1,8 @@ +ConsumerAdapterOptions | @mdf.js
                          ConsumerAdapterOptions:
                              | { config?: Adapters.Redis.Config; type: "redis" }
                              | { config?: Adapters.SocketIO.Config; type: "socketIO" }

                          Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, +consumer and adapter options must be provided, in other case the consumer will not be +started.

                          +

                          Type declaration

                          diff --git a/docs/types/_mdf.js_service-registry.CustomSetting.html b/docs/types/_mdf.js_service-registry.CustomSetting.html new file mode 100644 index 00000000..ffb8001d --- /dev/null +++ b/docs/types/_mdf.js_service-registry.CustomSetting.html @@ -0,0 +1,2 @@ +CustomSetting | @mdf.js
                          CustomSetting: any

                          Custom setting type

                          +
                          diff --git a/docs/types/_mdf.js_service-registry.CustomSettings.html b/docs/types/_mdf.js_service-registry.CustomSettings.html new file mode 100644 index 00000000..f13855d3 --- /dev/null +++ b/docs/types/_mdf.js_service-registry.CustomSettings.html @@ -0,0 +1,2 @@ +CustomSettings | @mdf.js
                          CustomSettings: Record<string, CustomSetting>

                          Custom settings type

                          +
                          diff --git a/docs/types/_mdf.js_service-registry.ErrorRecord.html b/docs/types/_mdf.js_service-registry.ErrorRecord.html new file mode 100644 index 00000000..77489acd --- /dev/null +++ b/docs/types/_mdf.js_service-registry.ErrorRecord.html @@ -0,0 +1,2 @@ +ErrorRecord | @mdf.js
                          diff --git a/docs/types/_mdf.js_service-registry._internal_.HandleableError.html b/docs/types/_mdf.js_service-registry._internal_.HandleableError.html new file mode 100644 index 00000000..d283bd49 --- /dev/null +++ b/docs/types/_mdf.js_service-registry._internal_.HandleableError.html @@ -0,0 +1 @@ +HandleableError | @mdf.js
                          HandleableError: Crash | Multi | Error
                          diff --git a/docs/types/_mdf.js_service-setup-provider.Setup.Provider.html b/docs/types/_mdf.js_service-setup-provider.Setup.Provider.html new file mode 100644 index 00000000..3a37871a --- /dev/null +++ b/docs/types/_mdf.js_service-setup-provider.Setup.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js

                          Type Parameters

                          • T extends Record<string, any> = Record<string, any>
                          diff --git a/docs/types/_mdf.js_service-setup-provider._internal_.FileEntry.html b/docs/types/_mdf.js_service-setup-provider._internal_.FileEntry.html new file mode 100644 index 00000000..e6e7fc9e --- /dev/null +++ b/docs/types/_mdf.js_service-setup-provider._internal_.FileEntry.html @@ -0,0 +1 @@ +FileEntry | @mdf.js
                          FileEntry: [string, string]
                          diff --git a/docs/types/_mdf.js_socket-client-provider.SocketIOClient.Provider.html b/docs/types/_mdf.js_socket-client-provider.SocketIOClient.Provider.html new file mode 100644 index 00000000..ef9b2f00 --- /dev/null +++ b/docs/types/_mdf.js_socket-client-provider.SocketIOClient.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                          diff --git a/docs/types/_mdf.js_socket-server-provider.SocketIOServer.Provider.html b/docs/types/_mdf.js_socket-server-provider.SocketIOServer.Provider.html new file mode 100644 index 00000000..af64cf45 --- /dev/null +++ b/docs/types/_mdf.js_socket-server-provider.SocketIOServer.Provider.html @@ -0,0 +1 @@ +Provider | @mdf.js
                          diff --git a/docs/types/_mdf.js_tasks.DefaultPollingGroups.html b/docs/types/_mdf.js_tasks.DefaultPollingGroups.html new file mode 100644 index 00000000..1e046c47 --- /dev/null +++ b/docs/types/_mdf.js_tasks.DefaultPollingGroups.html @@ -0,0 +1,2 @@ +DefaultPollingGroups | @mdf.js

                          Type Alias DefaultPollingGroups

                          DefaultPollingGroups:
                              | "1d"
                              | "12h"
                              | "6h"
                              | "4h"
                              | "1h"
                              | "30m"
                              | "15m"
                              | "10m"
                              | "5m"
                              | "1m"
                              | "30s"
                              | "10s"
                              | "5s"

                          Default period groups

                          +
                          diff --git a/docs/types/_mdf.js_tasks.DoneListener.html b/docs/types/_mdf.js_tasks.DoneListener.html new file mode 100644 index 00000000..b250d4a1 --- /dev/null +++ b/docs/types/_mdf.js_tasks.DoneListener.html @@ -0,0 +1,8 @@ +DoneListener | @mdf.js

                          Type Alias DoneListener<Result>

                          DoneListener: (
                              uuid: string,
                              result: Result,
                              meta: MetaData,
                              error?: Crash,
                          ) => void

                          Event handler for the done event, emitted when a task has ended, either due to completion or +failure.

                          +

                          Type Parameters

                          • Result = any

                            The type of the result of the task

                            +

                          Type declaration

                            • (uuid: string, result: Result, meta: MetaData, error?: Crash): void
                            • Parameters

                              • uuid: string

                                The unique identifier of the task

                                +
                              • result: Result

                                The result of the task

                                +
                              • meta: MetaData

                                The MetaData information of the task, including all the relevant information

                                +
                              • Optionalerror: Crash

                                The error of the task, if any

                                +

                              Returns void

                          diff --git a/docs/types/_mdf.js_tasks.MetricsDefinitions.html b/docs/types/_mdf.js_tasks.MetricsDefinitions.html new file mode 100644 index 00000000..d0416d3b --- /dev/null +++ b/docs/types/_mdf.js_tasks.MetricsDefinitions.html @@ -0,0 +1,2 @@ +MetricsDefinitions | @mdf.js

                          Type Alias MetricsDefinitions

                          Metrics definitions

                          +
                          diff --git a/docs/types/_mdf.js_tasks.PollingGroup.html b/docs/types/_mdf.js_tasks.PollingGroup.html new file mode 100644 index 00000000..a9622da8 --- /dev/null +++ b/docs/types/_mdf.js_tasks.PollingGroup.html @@ -0,0 +1,3 @@ +PollingGroup | @mdf.js

                          Type Alias PollingGroup

                          PollingGroup:
                              | `${number}d`
                              | `${number}h`
                              | `${number}m`
                              | `${number}s`
                              | `${number}ms`

                          Constraints the options for polling groups that should be expressed in seconds(s), minutes(m), +hours(h), or days(d)

                          +
                          diff --git a/docs/types/_mdf.js_tasks.RetryStrategy.html b/docs/types/_mdf.js_tasks.RetryStrategy.html new file mode 100644 index 00000000..fdf8282f --- /dev/null +++ b/docs/types/_mdf.js_tasks.RetryStrategy.html @@ -0,0 +1,2 @@ +RetryStrategy | @mdf.js

                          Type Alias RetryStrategy

                          RetryStrategy:
                              | FAIL_AFTER_EXECUTED
                              | FAIL_AFTER_SUCCESS
                              | NOT_EXEC_AFTER_SUCCESS
                              | RETRY

                          Represents the strategy to retry a task

                          +
                          diff --git a/docs/types/_mdf.js_tasks.ScanMetricsDefinitions.html b/docs/types/_mdf.js_tasks.ScanMetricsDefinitions.html new file mode 100644 index 00000000..e19eeed8 --- /dev/null +++ b/docs/types/_mdf.js_tasks.ScanMetricsDefinitions.html @@ -0,0 +1,13 @@ +ScanMetricsDefinitions | @mdf.js

                          Type Alias ScanMetricsDefinitions

                          ScanMetricsDefinitions: {
                              scan_cycle_duration_milliseconds: Histogram;
                              scan_cycles_on_stats: Gauge;
                              scan_cycles_total: Counter;
                              scan_duration_avg_milliseconds: Gauge;
                              scan_duration_max_milliseconds: Gauge;
                              scan_duration_min_milliseconds: Gauge;
                              scan_overruns_consecutive: Gauge;
                              scan_overruns_total: Counter;
                              scan_queue_size_max_allowed: Gauge;
                              scan_task_off_scan_total: Gauge;
                              scan_task_total: Gauge;
                          }

                          Metrics for Scan

                          +

                          Type declaration

                          • scan_cycle_duration_milliseconds: Histogram

                            Duration of scan cycle in milliseconds

                            +
                          • scan_cycles_on_stats: Gauge

                            Number of cycles used to generate the stats

                            +
                          • scan_cycles_total: Counter

                            Cumulative count of scan cycles performed.

                            +
                          • scan_duration_avg_milliseconds: Gauge

                            Average duration of scan cycles in milliseconds

                            +
                          • scan_duration_max_milliseconds: Gauge

                            Maximum duration of scan cycles in milliseconds

                            +
                          • scan_duration_min_milliseconds: Gauge

                            Minimum duration of scan cycles in milliseconds

                            +
                          • scan_overruns_consecutive: Gauge

                            Consecutive count of scan overruns

                            +
                          • scan_overruns_total: Counter

                            Cumulative count of scan overruns

                            +
                          • scan_queue_size_max_allowed: Gauge

                            Maximum number of task in the scan queue

                            +
                          • scan_task_off_scan_total: Gauge

                            Total number of task off scan

                            +
                          • scan_task_total: Gauge

                            Total number of task included in the scan

                            +
                          diff --git a/docs/types/_mdf.js_tasks.Strategy-1.html b/docs/types/_mdf.js_tasks.Strategy-1.html new file mode 100644 index 00000000..27c829af --- /dev/null +++ b/docs/types/_mdf.js_tasks.Strategy-1.html @@ -0,0 +1,2 @@ +Strategy | @mdf.js

                          Type Alias Strategy

                          Literal types for the strategy property

                          +
                          diff --git a/docs/types/_mdf.js_tasks.TaskBaseConfig.html b/docs/types/_mdf.js_tasks.TaskBaseConfig.html new file mode 100644 index 00000000..94389f1a --- /dev/null +++ b/docs/types/_mdf.js_tasks.TaskBaseConfig.html @@ -0,0 +1,2 @@ +TaskBaseConfig | @mdf.js

                          Type Alias TaskBaseConfig<Result, Binding>

                          Represents the base configuration for a task

                          +

                          Type Parameters

                          • Result = any
                          • Binding = any
                          diff --git a/docs/types/_mdf.js_tasks.TaskMetricsDefinitions.html b/docs/types/_mdf.js_tasks.TaskMetricsDefinitions.html new file mode 100644 index 00000000..5d0295d4 --- /dev/null +++ b/docs/types/_mdf.js_tasks.TaskMetricsDefinitions.html @@ -0,0 +1,6 @@ +TaskMetricsDefinitions | @mdf.js

                          Type Alias TaskMetricsDefinitions

                          TaskMetricsDefinitions: {
                              task_duration_milliseconds: Histogram;
                              task_errors_total: Counter;
                              task_in_progress: Gauge;
                              task_total: Counter;
                          }

                          Metrics for tasks

                          +

                          Type declaration

                          • task_duration_milliseconds: Histogram

                            Duration in millisecond for each concrete task, providing insights into task efficiency.

                            +
                          • task_errors_total: Counter

                            Cumulative count of task errors, providing insights into system reliability

                            +
                          • task_in_progress: Gauge

                            Number of task in progress

                            +
                          • task_total: Counter

                            Cumulative count of task performed, for tracking demand and usage patterns.

                            +
                          diff --git a/docs/types/_mdf.js_tasks.TaskState.html b/docs/types/_mdf.js_tasks.TaskState.html new file mode 100644 index 00000000..8450b69e --- /dev/null +++ b/docs/types/_mdf.js_tasks.TaskState.html @@ -0,0 +1,2 @@ +TaskState | @mdf.js

                          Type Alias TaskState

                          Represent the state of a task

                          +
                          diff --git a/docs/types/_mdf.js_utils.Coercible.html b/docs/types/_mdf.js_utils.Coercible.html new file mode 100644 index 00000000..6153e9a0 --- /dev/null +++ b/docs/types/_mdf.js_utils.Coercible.html @@ -0,0 +1,2 @@ +Coercible | @mdf.js

                          Type Alias Coercible

                          Coercible: boolean | number | Record<string, any> | any[] | null

                          Coercible types

                          +
                          diff --git a/docs/types/_mdf.js_utils.Format.html b/docs/types/_mdf.js_utils.Format.html new file mode 100644 index 00000000..6d74431d --- /dev/null +++ b/docs/types/_mdf.js_utils.Format.html @@ -0,0 +1,2 @@ +Format | @mdf.js
                          Format: "camelcase" | "pascalcase" | "lowercase" | "uppercase"

                          The format to apply to the environment variable name.

                          +
                          diff --git a/docs/types/_mdf.js_utils.FormatFunction.html b/docs/types/_mdf.js_utils.FormatFunction.html new file mode 100644 index 00000000..5a17f60f --- /dev/null +++ b/docs/types/_mdf.js_utils.FormatFunction.html @@ -0,0 +1,2 @@ +FormatFunction | @mdf.js

                          Type Alias FormatFunction

                          FormatFunction: (str: string) => string

                          Format function type

                          +

                          Type declaration

                            • (str: string): string
                            • Parameters

                              • str: string

                              Returns string

                          diff --git a/docs/types/_mdf.js_utils.LoggerFunction.html b/docs/types/_mdf.js_utils.LoggerFunction.html new file mode 100644 index 00000000..6e0d7ad1 --- /dev/null +++ b/docs/types/_mdf.js_utils.LoggerFunction.html @@ -0,0 +1,3 @@ +LoggerFunction | @mdf.js

                          Type Alias LoggerFunction

                          LoggerFunction: (error: Crash | Multi | Boom) => void

                          Represents a function that logs errors.

                          +

                          Type declaration

                          diff --git a/docs/types/_mdf.js_utils.LoggerInstance.html b/docs/types/_mdf.js_utils.LoggerInstance.html new file mode 100644 index 00000000..40cc51ac --- /dev/null +++ b/docs/types/_mdf.js_utils.LoggerInstance.html @@ -0,0 +1,2 @@ +LoggerInstance | @mdf.js

                          Type Alias LoggerInstance

                          LoggerInstance: { debug: (message: string) => void }

                          Logger instance

                          +

                          Type declaration

                          • debug: (message: string) => void
                          diff --git a/docs/types/_mdf.js_utils.TaskArguments.html b/docs/types/_mdf.js_utils.TaskArguments.html new file mode 100644 index 00000000..666287f8 --- /dev/null +++ b/docs/types/_mdf.js_utils.TaskArguments.html @@ -0,0 +1,2 @@ +TaskArguments | @mdf.js

                          Type Alias TaskArguments

                          TaskArguments: any[]

                          Type definition for the arguments of a task function.

                          +
                          diff --git a/docs/types/_mdf.js_utils.TaskAsPromise.html b/docs/types/_mdf.js_utils.TaskAsPromise.html new file mode 100644 index 00000000..0b97e63e --- /dev/null +++ b/docs/types/_mdf.js_utils.TaskAsPromise.html @@ -0,0 +1,3 @@ +TaskAsPromise | @mdf.js

                          Type Alias TaskAsPromise<R>

                          TaskAsPromise: (...args: TaskArguments) => Promise<R>

                          Represents a task that returns a promise.

                          +

                          Type Parameters

                          • R

                            The type of the result returned by the task.

                            +

                          Type declaration

                          diff --git a/docs/types/_mdf_js_amqp_provider.Receiver.Provider.html b/docs/types/_mdf_js_amqp_provider.Receiver.Provider.html deleted file mode 100644 index 0b5e9ae9..00000000 --- a/docs/types/_mdf_js_amqp_provider.Receiver.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Receiver, Config, Port>
                          diff --git a/docs/types/_mdf_js_amqp_provider.Sender.Provider.html b/docs/types/_mdf_js_amqp_provider.Sender.Provider.html deleted file mode 100644 index cecc4e18..00000000 --- a/docs/types/_mdf_js_amqp_provider.Sender.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<AwaitableSender, Config, Port>
                          diff --git a/docs/types/_mdf_js_core.Health.CheckEntry.html b/docs/types/_mdf_js_core.Health.CheckEntry.html deleted file mode 100644 index ff5ae25e..00000000 --- a/docs/types/_mdf_js_core.Health.CheckEntry.html +++ /dev/null @@ -1,2 +0,0 @@ -CheckEntry | @mdf.js
                          CheckEntry: `${ComponentName}:${MeasurementName}`

                          A check entry is a string that represents a unique key in the checks object

                          -
                          diff --git a/docs/types/_mdf_js_core.Health.Checks.html b/docs/types/_mdf_js_core.Health.Checks.html deleted file mode 100644 index f05c95e5..00000000 --- a/docs/types/_mdf_js_core.Health.Checks.html +++ /dev/null @@ -1,31 +0,0 @@ -Checks | @mdf.js
                          Checks<T>: {
                              [entry in CheckEntry]: Check<T>[]
                          }

                          The “checks” object MAY have a number of unique keys, one for each logical sub-components. -Since each sub-component may be backed by several nodes with varying health statuses, the key -points to an array of objects. In case of a single-node sub-component (or if presence of nodes -is not relevant), a single-element array should be used as the value, for consistency. -The key identifying an element in the object should be a unique string within the details -section. It MAY have two parts: {componentName}:{metricName}, in which case the meaning of -the parts SHOULD be as follows:

                          -
                            -
                          • componentName: Human-readable name for the component. MUST not contain a colon, in the name, -since colon is used as a separator
                          • -
                          • metricName: Name of the metrics that the status is reported for. MUST not contain a colon, -in the name, since colon is used as a separator and can be one of: -
                              -
                            • Pre-defined value from this spec. Pre-defined values include: -
                                -
                              • utilization
                              • -
                              • responseTime
                              • -
                              • connections
                              • -
                              • uptime
                              • -
                              -
                            • -
                            • A common and standard term from a well-known source such as schema.org, IANA or -microformats.
                            • -
                            • A URI that indicates extra semantics and processing rules that MAY be provided by a -resource at the other end of the URI. URIs do not have to be dereferenceable, however. -They are just a namespace, and the meaning of a namespace CAN be provided by any -convenient means (e.g. publishing an RFC, Swagger document or a nicely printed book).
                            • -
                            -
                          • -
                          -

                          Type Parameters

                          • T = any
                          diff --git a/docs/types/_mdf_js_core.Health.ComponentName.html b/docs/types/_mdf_js_core.Health.ComponentName.html deleted file mode 100644 index 2bb54ee3..00000000 --- a/docs/types/_mdf_js_core.Health.ComponentName.html +++ /dev/null @@ -1,2 +0,0 @@ -ComponentName | @mdf.js
                          ComponentName: string

                          String alias type for a component name

                          -
                          diff --git a/docs/types/_mdf_js_core.Health.MeasurementName.html b/docs/types/_mdf_js_core.Health.MeasurementName.html deleted file mode 100644 index 389e2841..00000000 --- a/docs/types/_mdf_js_core.Health.MeasurementName.html +++ /dev/null @@ -1,2 +0,0 @@ -MeasurementName | @mdf.js
                          MeasurementName: string

                          String alias type for a measurement name

                          -
                          diff --git a/docs/types/_mdf_js_core.Health.Status-1.html b/docs/types/_mdf_js_core.Health.Status-1.html deleted file mode 100644 index 51db9224..00000000 --- a/docs/types/_mdf_js_core.Health.Status-1.html +++ /dev/null @@ -1,2 +0,0 @@ -Status | @mdf.js
                          Status: typeof STATUSES[number]

                          Service status

                          -
                          diff --git a/docs/types/_mdf_js_core.Jobs.AnyHeaders.html b/docs/types/_mdf_js_core.Jobs.AnyHeaders.html deleted file mode 100644 index 34878519..00000000 --- a/docs/types/_mdf_js_core.Jobs.AnyHeaders.html +++ /dev/null @@ -1,2 +0,0 @@ -AnyHeaders | @mdf.js
                          AnyHeaders: Record<string, any>

                          Any other extra header information

                          -
                          diff --git a/docs/types/_mdf_js_core.Jobs.AnyOptions.html b/docs/types/_mdf_js_core.Jobs.AnyOptions.html deleted file mode 100644 index 6cf7e501..00000000 --- a/docs/types/_mdf_js_core.Jobs.AnyOptions.html +++ /dev/null @@ -1,2 +0,0 @@ -AnyOptions | @mdf.js
                          AnyOptions: Record<string, any>

                          Any other extra option

                          -
                          diff --git a/docs/types/_mdf_js_core.Jobs.DoneEventHandler.html b/docs/types/_mdf_js_core.Jobs.DoneEventHandler.html deleted file mode 100644 index 834029a9..00000000 --- a/docs/types/_mdf_js_core.Jobs.DoneEventHandler.html +++ /dev/null @@ -1,7 +0,0 @@ -DoneEventHandler | @mdf.js

                          Type Alias DoneEventHandler<Type>

                          DoneEventHandler<Type>: ((uuid: string, result: Result<Type>, error?: Multi) => void)

                          Event handler for the done event, emitted when a job has ended, either due to completion or -failure.

                          -

                          Type Parameters

                          • Type extends string

                            The type of the job

                            -

                          Type declaration

                            • (uuid, result, error?): void
                            • Parameters

                              • uuid: string

                                The unique identifier of the job

                                -
                              • result: Result<Type>

                                The result of the job

                                -
                              • Optionalerror: Multi

                                The error of the job, if any

                                -

                              Returns void

                          diff --git a/docs/types/_mdf_js_core.Jobs.Headers.html b/docs/types/_mdf_js_core.Jobs.Headers.html deleted file mode 100644 index a239059e..00000000 --- a/docs/types/_mdf_js_core.Jobs.Headers.html +++ /dev/null @@ -1,2 +0,0 @@ -Headers | @mdf.js

                          Type Alias Headers<T>

                          Headers<T>: T

                          Job headers

                          -

                          Type Parameters

                          diff --git a/docs/types/_mdf_js_core.Jobs.Options.html b/docs/types/_mdf_js_core.Jobs.Options.html deleted file mode 100644 index ce52c961..00000000 --- a/docs/types/_mdf_js_core.Jobs.Options.html +++ /dev/null @@ -1,2 +0,0 @@ -Options | @mdf.js

                          Type Alias Options<CustomHeaders, CustomOptions>

                          Job options

                          -

                          Type Parameters

                          • CustomHeaders extends Record<string, any> = AnyHeaders
                          • CustomOptions extends Record<string, any> = AnyOptions
                          diff --git a/docs/types/_mdf_js_core.Layer.Observable.html b/docs/types/_mdf_js_core.Layer.Observable.html deleted file mode 100644 index f5edad77..00000000 --- a/docs/types/_mdf_js_core.Layer.Observable.html +++ /dev/null @@ -1,2 +0,0 @@ -Observable | @mdf.js
                          Observable:
                              | Manager<any, any, any>
                              | Component
                              | Resource
                              | Service

                          Represents an observable entity that can be monitored.

                          -
                          diff --git a/docs/types/_mdf_js_core.Layer.Provider.ProviderState.html b/docs/types/_mdf_js_core.Layer.Provider.ProviderState.html deleted file mode 100644 index 80cb5688..00000000 --- a/docs/types/_mdf_js_core.Layer.Provider.ProviderState.html +++ /dev/null @@ -1,2 +0,0 @@ -ProviderState | @mdf.js
                          ProviderState: typeof PROVIDER_STATES[number]

                          Provider state type

                          -
                          diff --git a/docs/types/_mdf_js_crash.Cause.html b/docs/types/_mdf_js_crash.Cause.html deleted file mode 100644 index 3ac49cf4..00000000 --- a/docs/types/_mdf_js_crash.Cause.html +++ /dev/null @@ -1,2 +0,0 @@ -Cause | @mdf.js
                          Cause: Error | Crash | Multi

                          Error cause

                          -
                          diff --git a/docs/types/_mdf_js_crash.ContextLink.html b/docs/types/_mdf_js_crash.ContextLink.html deleted file mode 100644 index 00ec4a7a..00000000 --- a/docs/types/_mdf_js_crash.ContextLink.html +++ /dev/null @@ -1,2 +0,0 @@ -ContextLink | @mdf.js

                          Type Alias ContextLink

                          ContextLink: {
                              [context: string]: SimpleLink;
                          }

                          Context links, expressed as map of key-value pairs

                          -
                          diff --git a/docs/types/_mdf_js_crash.Links.html b/docs/types/_mdf_js_crash.Links.html deleted file mode 100644 index 570663a7..00000000 --- a/docs/types/_mdf_js_crash.Links.html +++ /dev/null @@ -1,2 +0,0 @@ -Links | @mdf.js
                          Links: {
                              [link: string]: SimpleLink | ContextLink;
                          }

                          Links that leads to further details about this particular occurrence of the problem.

                          -
                          diff --git a/docs/types/_mdf_js_crash.SimpleLink.html b/docs/types/_mdf_js_crash.SimpleLink.html deleted file mode 100644 index 5f6d20d0..00000000 --- a/docs/types/_mdf_js_crash.SimpleLink.html +++ /dev/null @@ -1,2 +0,0 @@ -SimpleLink | @mdf.js

                          Type Alias SimpleLink

                          SimpleLink: string

                          Simple link expressed as a regular string

                          -
                          diff --git a/docs/types/_mdf_js_doorkeeper.ResultCallback.html b/docs/types/_mdf_js_doorkeeper.ResultCallback.html deleted file mode 100644 index 53fc6580..00000000 --- a/docs/types/_mdf_js_doorkeeper.ResultCallback.html +++ /dev/null @@ -1,2 +0,0 @@ -ResultCallback | @mdf.js

                          Type Alias ResultCallback<T, K>

                          ResultCallback<T, K>: ((error?: Crash | Multi, result?: ValidatedOutput<T, K>) => void)

                          Callback function for the validation process

                          -

                          Type Parameters

                          • T
                          • K
                          diff --git a/docs/types/_mdf_js_doorkeeper.SchemaSelector.html b/docs/types/_mdf_js_doorkeeper.SchemaSelector.html deleted file mode 100644 index 5ee81120..00000000 --- a/docs/types/_mdf_js_doorkeeper.SchemaSelector.html +++ /dev/null @@ -1 +0,0 @@ -SchemaSelector | @mdf.js

                          Type Alias SchemaSelector<T>

                          SchemaSelector<T>: T extends void
                              ? string
                              : keyof T & string

                          Type Parameters

                          • T
                          diff --git a/docs/types/_mdf_js_doorkeeper.ValidatedOutput.html b/docs/types/_mdf_js_doorkeeper.ValidatedOutput.html deleted file mode 100644 index 2e5fe64d..00000000 --- a/docs/types/_mdf_js_doorkeeper.ValidatedOutput.html +++ /dev/null @@ -1 +0,0 @@ -ValidatedOutput | @mdf.js

                          Type Alias ValidatedOutput<T, K>

                          ValidatedOutput<T, K>: K extends keyof T
                              ? T[K]
                              : any

                          Type Parameters

                          • T
                          • K
                          diff --git a/docs/types/_mdf_js_elastic_provider.Elastic.Provider.html b/docs/types/_mdf_js_elastic_provider.Elastic.Provider.html deleted file mode 100644 index c1fb31e4..00000000 --- a/docs/types/_mdf_js_elastic_provider.Elastic.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, Config, Elastic.Port>
                          diff --git a/docs/types/_mdf_js_faker.Builder.html b/docs/types/_mdf_js_faker.Builder.html deleted file mode 100644 index f5a8107e..00000000 --- a/docs/types/_mdf_js_faker.Builder.html +++ /dev/null @@ -1,2 +0,0 @@ -Builder | @mdf.js

                          Type Alias Builder<T, K>

                          Builder<T, K>: ((...args: any) => T[K] | undefined)

                          Type for function for attribute builder function

                          -

                          Type Parameters

                          • T
                          • K extends keyof T
                          diff --git a/docs/types/_mdf_js_faker.DefaultValue.html b/docs/types/_mdf_js_faker.DefaultValue.html deleted file mode 100644 index fac1b4ab..00000000 --- a/docs/types/_mdf_js_faker.DefaultValue.html +++ /dev/null @@ -1,2 +0,0 @@ -DefaultValue | @mdf.js

                          Type Alias DefaultValue<T, K>

                          DefaultValue<T, K>: T[K]

                          Type for attribute default value

                          -

                          Type Parameters

                          • T
                          • K extends keyof T
                          diff --git a/docs/types/_mdf_js_faker.Dependencies.html b/docs/types/_mdf_js_faker.Dependencies.html deleted file mode 100644 index 912f149f..00000000 --- a/docs/types/_mdf_js_faker.Dependencies.html +++ /dev/null @@ -1,2 +0,0 @@ -Dependencies | @mdf.js

                          Type Alias Dependencies<T, K>

                          Dependencies<T, K>: (K | string)[]

                          Type for attribute dependencies

                          -

                          Type Parameters

                          • T
                          • K extends keyof T
                          diff --git a/docs/types/_mdf_js_firehose.JobEventHandler.html b/docs/types/_mdf_js_firehose.JobEventHandler.html deleted file mode 100644 index 7c2a9bcf..00000000 --- a/docs/types/_mdf_js_firehose.JobEventHandler.html +++ /dev/null @@ -1,2 +0,0 @@ -JobEventHandler | @mdf.js

                          Type Alias JobEventHandler<Type, Data, CustomHeaders, CustomOptions>

                          Firehose job event handler

                          -

                          Type Parameters

                          • Type extends string = string
                          • Data = any
                          • CustomHeaders extends Record<string, any> = NoMoreHeaders
                          • CustomOptions extends Record<string, any> = NoMoreOptions
                          diff --git a/docs/types/_mdf_js_firehose.Plugs.Sink.Any.html b/docs/types/_mdf_js_firehose.Plugs.Sink.Any.html deleted file mode 100644 index 427fd03e..00000000 --- a/docs/types/_mdf_js_firehose.Plugs.Sink.Any.html +++ /dev/null @@ -1 +0,0 @@ -Any | @mdf.js

                          Type Alias Any<Type, Data, CustomHeaders, CustomOptions>

                          Type Parameters

                          • Type extends string = string
                          • Data = any
                          • CustomHeaders extends Record<string, any> = AnyHeaders
                          • CustomOptions extends Record<string, any> = AnyOptions
                          diff --git a/docs/types/_mdf_js_firehose.Plugs.Sink.Tap.html b/docs/types/_mdf_js_firehose.Plugs.Sink.Tap.html deleted file mode 100644 index 32408565..00000000 --- a/docs/types/_mdf_js_firehose.Plugs.Sink.Tap.html +++ /dev/null @@ -1 +0,0 @@ -Tap | @mdf.js

                          Type Alias Tap<Type, Data, CustomHeaders, CustomOptions>

                          Type Parameters

                          • Type extends string = string
                          • Data = any
                          • CustomHeaders extends Record<string, any> = AnyHeaders
                          • CustomOptions extends Record<string, any> = AnyOptions
                          diff --git a/docs/types/_mdf_js_firehose.Plugs.Source.Any.html b/docs/types/_mdf_js_firehose.Plugs.Source.Any.html deleted file mode 100644 index 901ec3bf..00000000 --- a/docs/types/_mdf_js_firehose.Plugs.Source.Any.html +++ /dev/null @@ -1 +0,0 @@ -Any | @mdf.js

                          Type Alias Any<Type, Data, CustomHeaders, CustomOptions>

                          Type Parameters

                          • Type extends string = string
                          • Data = any
                          • CustomHeaders extends Record<string, any> = NoMoreHeaders
                          • CustomOptions extends Record<string, any> = NoMoreOptions
                          diff --git a/docs/types/_mdf_js_http_client_provider.HTTP.Provider.html b/docs/types/_mdf_js_http_client_provider.HTTP.Provider.html deleted file mode 100644 index f891f167..00000000 --- a/docs/types/_mdf_js_http_client_provider.HTTP.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, HTTP.Config, Port>
                          diff --git a/docs/types/_mdf_js_http_server_provider.HTTP.Provider.html b/docs/types/_mdf_js_http_server_provider.HTTP.Provider.html deleted file mode 100644 index 690d3cba..00000000 --- a/docs/types/_mdf_js_http_server_provider.HTTP.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Server, HTTP.Config, Port>
                          diff --git a/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Config.html b/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Config.html deleted file mode 100644 index 7c5e001b..00000000 --- a/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Config.html +++ /dev/null @@ -1 +0,0 @@ -Config | @mdf.js
                          diff --git a/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Provider.html b/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Provider.html deleted file mode 100644 index 01bef2e1..00000000 --- a/docs/types/_mdf_js_jsonl_archiver.JSONLArchiver.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          diff --git a/docs/types/_mdf_js_kafka_provider.Consumer.Provider.html b/docs/types/_mdf_js_kafka_provider.Consumer.Provider.html deleted file mode 100644 index 2d6a2fb7..00000000 --- a/docs/types/_mdf_js_kafka_provider.Consumer.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Consumer, Consumer.Config, Port>
                          diff --git a/docs/types/_mdf_js_kafka_provider.Producer.Provider.html b/docs/types/_mdf_js_kafka_provider.Producer.Provider.html deleted file mode 100644 index 91512deb..00000000 --- a/docs/types/_mdf_js_kafka_provider.Producer.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Producer, Producer.Config, Port>
                          diff --git a/docs/types/_mdf_js_logger.FluentdTransportConfig.html b/docs/types/_mdf_js_logger.FluentdTransportConfig.html deleted file mode 100644 index 5bcbaddb..00000000 --- a/docs/types/_mdf_js_logger.FluentdTransportConfig.html +++ /dev/null @@ -1,4 +0,0 @@ -FluentdTransportConfig | @mdf.js

                          Type Alias FluentdTransportConfig

                          FluentdTransportConfig: Exclude<Options, "internalLogger"> & {
                              enabled?: boolean;
                              level?: LogLevel;
                          }

                          Fluentd transport configuration

                          -

                          Type declaration

                          • Optionalenabled?: boolean

                            Fluentd transport enabled, default: false

                            -
                          • Optionallevel?: LogLevel

                            Fluentd log level, default: info

                            -
                          diff --git a/docs/types/_mdf_js_middlewares.AfterRoutesMiddlewares.html b/docs/types/_mdf_js_middlewares.AfterRoutesMiddlewares.html deleted file mode 100644 index 95296860..00000000 --- a/docs/types/_mdf_js_middlewares.AfterRoutesMiddlewares.html +++ /dev/null @@ -1 +0,0 @@ -AfterRoutesMiddlewares | @mdf.js

                          Type Alias AfterRoutesMiddlewares

                          AfterRoutesMiddlewares: ErrorHandler | Default
                          diff --git a/docs/types/_mdf_js_middlewares.AuditCategory.html b/docs/types/_mdf_js_middlewares.AuditCategory.html deleted file mode 100644 index dc13b6ff..00000000 --- a/docs/types/_mdf_js_middlewares.AuditCategory.html +++ /dev/null @@ -1,4 +0,0 @@ -AuditCategory | @mdf.js
                          AuditCategory:
                              | "create"
                              | "modify"
                              | "execute"
                              | "access"
                              | "delete"

                          Copyright 2024 Mytra Control S.L. All rights reserved.

                          -

                          Use of this source code is governed by an MIT-style license that can be found in the LICENSE file -or at https://opensource.org/licenses/MIT.

                          -
                          diff --git a/docs/types/_mdf_js_middlewares.BeforeRoutesMiddlewares.html b/docs/types/_mdf_js_middlewares.BeforeRoutesMiddlewares.html deleted file mode 100644 index 59d5ff68..00000000 --- a/docs/types/_mdf_js_middlewares.BeforeRoutesMiddlewares.html +++ /dev/null @@ -1 +0,0 @@ -BeforeRoutesMiddlewares | @mdf.js

                          Type Alias BeforeRoutesMiddlewares

                          BeforeRoutesMiddlewares:
                              | Audit
                              | AuthZ
                              | BodyParser
                              | Cache
                              | Cors
                              | Security
                              | Logger
                              | Metrics
                              | Multer
                              | NoCache
                              | RateLimiter
                              | RequestId
                          diff --git a/docs/types/_mdf_js_middlewares.EndpointsMiddlewares.html b/docs/types/_mdf_js_middlewares.EndpointsMiddlewares.html deleted file mode 100644 index 60553246..00000000 --- a/docs/types/_mdf_js_middlewares.EndpointsMiddlewares.html +++ /dev/null @@ -1 +0,0 @@ -EndpointsMiddlewares | @mdf.js

                          Type Alias EndpointsMiddlewares

                          EndpointsMiddlewares:
                              | Audit
                              | AuthZ
                              | Cache
                              | NoCache
                              | RateLimiter
                          diff --git a/docs/types/_mdf_js_middlewares.Middlewares.html b/docs/types/_mdf_js_middlewares.Middlewares.html deleted file mode 100644 index e95d6c49..00000000 --- a/docs/types/_mdf_js_middlewares.Middlewares.html +++ /dev/null @@ -1 +0,0 @@ -Middlewares | @mdf.js
                          Middlewares:
                              | Audit
                              | AuthZ
                              | BodyParser
                              | Cache
                              | Cors
                              | Default
                              | ErrorHandler
                              | Security
                              | Logger
                              | Metrics
                              | Multer
                              | NoCache
                              | RateLimiter
                              | RequestId
                          diff --git a/docs/types/_mdf_js_mongo_provider.Mongo.Provider.html b/docs/types/_mdf_js_mongo_provider.Mongo.Provider.html deleted file mode 100644 index fed00b6c..00000000 --- a/docs/types/_mdf_js_mongo_provider.Mongo.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, Mongo.Config, Port>
                          diff --git a/docs/types/_mdf_js_mqtt_provider.MQTT.Provider.html b/docs/types/_mdf_js_mqtt_provider.MQTT.Provider.html deleted file mode 100644 index 95ee6e93..00000000 --- a/docs/types/_mdf_js_mqtt_provider.MQTT.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, MQTT.Config, Port>
                          diff --git a/docs/types/_mdf_js_openc2.Adapters.Dummy.Config.html b/docs/types/_mdf_js_openc2.Adapters.Dummy.Config.html deleted file mode 100644 index ba25ec00..00000000 --- a/docs/types/_mdf_js_openc2.Adapters.Dummy.Config.html +++ /dev/null @@ -1 +0,0 @@ -Config | @mdf.js
                          Config: {}
                          diff --git a/docs/types/_mdf_js_openc2.Adapters.Redis.Config.html b/docs/types/_mdf_js_openc2.Adapters.Redis.Config.html deleted file mode 100644 index e7ac2cce..00000000 --- a/docs/types/_mdf_js_openc2.Adapters.Redis.Config.html +++ /dev/null @@ -1 +0,0 @@ -Config | @mdf.js
                          diff --git a/docs/types/_mdf_js_openc2.Adapters.SocketIO.Config.html b/docs/types/_mdf_js_openc2.Adapters.SocketIO.Config.html deleted file mode 100644 index 4de49743..00000000 --- a/docs/types/_mdf_js_openc2.Adapters.SocketIO.Config.html +++ /dev/null @@ -1 +0,0 @@ -Config | @mdf.js
                          diff --git a/docs/types/_mdf_js_openc2_core.CommandJobDone.html b/docs/types/_mdf_js_openc2_core.CommandJobDone.html deleted file mode 100644 index 117fec8c..00000000 --- a/docs/types/_mdf_js_openc2_core.CommandJobDone.html +++ /dev/null @@ -1 +0,0 @@ -CommandJobDone | @mdf.js
                          CommandJobDone: Result<"command"> & {
                              command: CommandMessage;
                          }
                          diff --git a/docs/types/_mdf_js_openc2_core.CommandJobHandler.html b/docs/types/_mdf_js_openc2_core.CommandJobHandler.html deleted file mode 100644 index 9d4794e1..00000000 --- a/docs/types/_mdf_js_openc2_core.CommandJobHandler.html +++ /dev/null @@ -1 +0,0 @@ -CommandJobHandler | @mdf.js
                          CommandJobHandler: JobHandler<"command", CommandMessage, CommandJobHeader>
                          diff --git a/docs/types/_mdf_js_openc2_core.Control.ActionTargetPairs.html b/docs/types/_mdf_js_openc2_core.Control.ActionTargetPairs.html deleted file mode 100644 index 4b4482a0..00000000 --- a/docs/types/_mdf_js_openc2_core.Control.ActionTargetPairs.html +++ /dev/null @@ -1,2 +0,0 @@ -ActionTargetPairs | @mdf.js
                          ActionTargetPairs: {
                              [Property in ActionType]?: string[]
                          }

                          Map of each action supported by this actuator to the list of targets applicable to that action

                          -
                          diff --git a/docs/types/_mdf_js_openc2_core.Control.ActionType.html b/docs/types/_mdf_js_openc2_core.Control.ActionType.html deleted file mode 100644 index 32d44288..00000000 --- a/docs/types/_mdf_js_openc2_core.Control.ActionType.html +++ /dev/null @@ -1 +0,0 @@ -ActionType | @mdf.js
                          ActionType: `${Action}`
                          diff --git a/docs/types/_mdf_js_openc2_core.Control.Message.html b/docs/types/_mdf_js_openc2_core.Control.Message.html deleted file mode 100644 index 0ae07079..00000000 --- a/docs/types/_mdf_js_openc2_core.Control.Message.html +++ /dev/null @@ -1 +0,0 @@ -Message | @mdf.js
                          diff --git a/docs/types/_mdf_js_openc2_core.Control.Namespace.html b/docs/types/_mdf_js_openc2_core.Control.Namespace.html deleted file mode 100644 index 6e5629e4..00000000 --- a/docs/types/_mdf_js_openc2_core.Control.Namespace.html +++ /dev/null @@ -1,2 +0,0 @@ -Namespace | @mdf.js
                          Namespace: `x-${string}`

                          Namespace type

                          -
                          diff --git a/docs/types/_mdf_js_openc2_core.OnCommandHandler.html b/docs/types/_mdf_js_openc2_core.OnCommandHandler.html deleted file mode 100644 index b1146105..00000000 --- a/docs/types/_mdf_js_openc2_core.OnCommandHandler.html +++ /dev/null @@ -1 +0,0 @@ -OnCommandHandler | @mdf.js
                          OnCommandHandler: ((message: CommandMessage, done: ((error?: Crash | Error, message?: ResponseMessage) => void)) => void)
                          diff --git a/docs/types/_mdf_js_openc2_core.Resolver.html b/docs/types/_mdf_js_openc2_core.Resolver.html deleted file mode 100644 index b676ab7a..00000000 --- a/docs/types/_mdf_js_openc2_core.Resolver.html +++ /dev/null @@ -1 +0,0 @@ -Resolver | @mdf.js
                          Resolver: (<T>(target: T) => Promise<AllowedResultPropertyTypes | void>)
                          diff --git a/docs/types/_mdf_js_openc2_core.ResolverEntry.html b/docs/types/_mdf_js_openc2_core.ResolverEntry.html deleted file mode 100644 index 912826f9..00000000 --- a/docs/types/_mdf_js_openc2_core.ResolverEntry.html +++ /dev/null @@ -1 +0,0 @@ -ResolverEntry | @mdf.js
                          ResolverEntry: `${ActionType}:${Namespace}:${string}`
                          diff --git a/docs/types/_mdf_js_openc2_core.ResolverMap.html b/docs/types/_mdf_js_openc2_core.ResolverMap.html deleted file mode 100644 index 1ef71426..00000000 --- a/docs/types/_mdf_js_openc2_core.ResolverMap.html +++ /dev/null @@ -1 +0,0 @@ -ResolverMap | @mdf.js
                          ResolverMap: {
                              [entry in ResolverEntry]?: Resolver
                          }
                          diff --git a/docs/types/_mdf_js_redis_provider.Redis.Config.html b/docs/types/_mdf_js_redis_provider.Redis.Config.html deleted file mode 100644 index 26fad158..00000000 --- a/docs/types/_mdf_js_redis_provider.Redis.Config.html +++ /dev/null @@ -1 +0,0 @@ -Config | @mdf.js
                          Config: Omit<RedisOptions, "keepAlive"> & {
                              checkInterval?: number;
                              disableChecks?: boolean;
                              keepAlive?: number;
                          }
                          diff --git a/docs/types/_mdf_js_redis_provider.Redis.Provider.html b/docs/types/_mdf_js_redis_provider.Redis.Provider.html deleted file mode 100644 index cd238414..00000000 --- a/docs/types/_mdf_js_redis_provider.Redis.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, Redis.Config, Port>
                          diff --git a/docs/types/_mdf_js_s3_provider.S3.Provider.html b/docs/types/_mdf_js_s3_provider.S3.Provider.html deleted file mode 100644 index 597b637e..00000000 --- a/docs/types/_mdf_js_s3_provider.S3.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider: Manager<Client, Config, Port>
                          diff --git a/docs/types/_mdf_js_service_registry.ConsumerAdapterOptions.html b/docs/types/_mdf_js_service_registry.ConsumerAdapterOptions.html deleted file mode 100644 index c81a0129..00000000 --- a/docs/types/_mdf_js_service_registry.ConsumerAdapterOptions.html +++ /dev/null @@ -1,8 +0,0 @@ -ConsumerAdapterOptions | @mdf.js
                          ConsumerAdapterOptions: {
                              config?: Adapters.Redis.Config;
                              type: "redis";
                          } | {
                              config?: Adapters.SocketIO.Config;
                              type: "socketIO";
                          }

                          Consumer adapter options: Redis or SocketIO. In order to configure the consumer instance, -consumer and adapter options must be provided, in other case the consumer will not be -started.

                          -

                          Type declaration

                          • Optionalconfig?: Adapters.Redis.Config

                            Redis adapter configuration

                            -
                          • type: "redis"

                            Adapter type: Redis

                            -

                          Type declaration

                          • Optionalconfig?: Adapters.SocketIO.Config

                            SocketIO adapter configuration

                            -
                          • type: "socketIO"

                            Adapter type: SocketIO

                            -
                          diff --git a/docs/types/_mdf_js_service_registry.CustomSetting.html b/docs/types/_mdf_js_service_registry.CustomSetting.html deleted file mode 100644 index 1883fb99..00000000 --- a/docs/types/_mdf_js_service_registry.CustomSetting.html +++ /dev/null @@ -1,2 +0,0 @@ -CustomSetting | @mdf.js
                          CustomSetting: any

                          Custom setting type

                          -
                          diff --git a/docs/types/_mdf_js_service_registry.CustomSettings.html b/docs/types/_mdf_js_service_registry.CustomSettings.html deleted file mode 100644 index b8679cc0..00000000 --- a/docs/types/_mdf_js_service_registry.CustomSettings.html +++ /dev/null @@ -1,2 +0,0 @@ -CustomSettings | @mdf.js
                          CustomSettings: Record<string, CustomSetting>

                          Custom settings type

                          -
                          diff --git a/docs/types/_mdf_js_service_registry.ErrorRecord.html b/docs/types/_mdf_js_service_registry.ErrorRecord.html deleted file mode 100644 index 89bc9cfe..00000000 --- a/docs/types/_mdf_js_service_registry.ErrorRecord.html +++ /dev/null @@ -1 +0,0 @@ -ErrorRecord | @mdf.js
                          ErrorRecord: ExtendedCrashObject | ExtendedMultiObject
                          diff --git a/docs/types/_mdf_js_service_setup_provider.Setup.Provider.html b/docs/types/_mdf_js_service_setup_provider.Setup.Provider.html deleted file mode 100644 index 1566b3d5..00000000 --- a/docs/types/_mdf_js_service_setup_provider.Setup.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          Provider<T>: Manager<ConfigManager<T>, Setup.Config, Port<T>>

                          Type Parameters

                          • T extends Record<string, any> = Record<string, any>
                          diff --git a/docs/types/_mdf_js_socket_client_provider.SocketIOClient.Provider.html b/docs/types/_mdf_js_socket_client_provider.SocketIOClient.Provider.html deleted file mode 100644 index e0d18fcd..00000000 --- a/docs/types/_mdf_js_socket_client_provider.SocketIOClient.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          diff --git a/docs/types/_mdf_js_socket_server_provider.SocketIOServer.Provider.html b/docs/types/_mdf_js_socket_server_provider.SocketIOServer.Provider.html deleted file mode 100644 index 98828ad5..00000000 --- a/docs/types/_mdf_js_socket_server_provider.SocketIOServer.Provider.html +++ /dev/null @@ -1 +0,0 @@ -Provider | @mdf.js
                          diff --git a/docs/types/_mdf_js_tasks.DefaultPollingGroups.html b/docs/types/_mdf_js_tasks.DefaultPollingGroups.html deleted file mode 100644 index 0035450d..00000000 --- a/docs/types/_mdf_js_tasks.DefaultPollingGroups.html +++ /dev/null @@ -1,2 +0,0 @@ -DefaultPollingGroups | @mdf.js

                          Type Alias DefaultPollingGroups

                          DefaultPollingGroups:
                              | "1d"
                              | "12h"
                              | "6h"
                              | "4h"
                              | "1h"
                              | "30m"
                              | "15m"
                              | "10m"
                              | "5m"
                              | "1m"
                              | "30s"
                              | "10s"
                              | "5s"

                          Default period groups

                          -
                          diff --git a/docs/types/_mdf_js_tasks.DoneListener.html b/docs/types/_mdf_js_tasks.DoneListener.html deleted file mode 100644 index 7a4fad4e..00000000 --- a/docs/types/_mdf_js_tasks.DoneListener.html +++ /dev/null @@ -1,8 +0,0 @@ -DoneListener | @mdf.js

                          Type Alias DoneListener<Result>

                          DoneListener<Result>: ((uuid: string, result: Result, meta: MetaData, error?: Crash) => void)

                          Event handler for the done event, emitted when a task has ended, either due to completion or -failure.

                          -

                          Type Parameters

                          • Result = any

                            The type of the result of the task

                            -

                          Type declaration

                            • (uuid, result, meta, error?): void
                            • Parameters

                              • uuid: string

                                The unique identifier of the task

                                -
                              • result: Result

                                The result of the task

                                -
                              • meta: MetaData

                                The MetaData information of the task, including all the relevant information

                                -
                              • Optionalerror: Crash

                                The error of the task, if any

                                -

                              Returns void

                          diff --git a/docs/types/_mdf_js_tasks.MetricsDefinitions.html b/docs/types/_mdf_js_tasks.MetricsDefinitions.html deleted file mode 100644 index 3fc70873..00000000 --- a/docs/types/_mdf_js_tasks.MetricsDefinitions.html +++ /dev/null @@ -1,2 +0,0 @@ -MetricsDefinitions | @mdf.js

                          Type Alias MetricsDefinitions

                          MetricsDefinitions: TaskMetricsDefinitions & ScanMetricsDefinitions

                          Metrics definitions

                          -
                          diff --git a/docs/types/_mdf_js_tasks.PollingGroup.html b/docs/types/_mdf_js_tasks.PollingGroup.html deleted file mode 100644 index 7660c6d1..00000000 --- a/docs/types/_mdf_js_tasks.PollingGroup.html +++ /dev/null @@ -1,3 +0,0 @@ -PollingGroup | @mdf.js

                          Type Alias PollingGroup

                          PollingGroup:
                              | `${number}d`
                              | `${number}h`
                              | `${number}m`
                              | `${number}s`
                              | `${number}ms`

                          Constraints the options for polling groups that should be expressed in seconds(s), minutes(m), -hours(h), or days(d)

                          -
                          diff --git a/docs/types/_mdf_js_tasks.RetryStrategy.html b/docs/types/_mdf_js_tasks.RetryStrategy.html deleted file mode 100644 index 9bfdad84..00000000 --- a/docs/types/_mdf_js_tasks.RetryStrategy.html +++ /dev/null @@ -1,2 +0,0 @@ -RetryStrategy | @mdf.js

                          Type Alias RetryStrategy

                          RetryStrategy:
                              | FAIL_AFTER_EXECUTED
                              | FAIL_AFTER_SUCCESS
                              | NOT_EXEC_AFTER_SUCCESS
                              | RETRY

                          Represents the strategy to retry a task

                          -
                          diff --git a/docs/types/_mdf_js_tasks.Strategy-1.html b/docs/types/_mdf_js_tasks.Strategy-1.html deleted file mode 100644 index 87280a13..00000000 --- a/docs/types/_mdf_js_tasks.Strategy-1.html +++ /dev/null @@ -1,2 +0,0 @@ -Strategy | @mdf.js

                          Type Alias Strategy

                          Strategy:
                              | LEAK
                              | OVERFLOW
                              | OVERFLOW_PRIORITY
                              | BLOCK

                          Literal types for the strategy property

                          -
                          diff --git a/docs/types/_mdf_js_tasks.TaskBaseConfig.html b/docs/types/_mdf_js_tasks.TaskBaseConfig.html deleted file mode 100644 index 677d1a0e..00000000 --- a/docs/types/_mdf_js_tasks.TaskBaseConfig.html +++ /dev/null @@ -1,2 +0,0 @@ -TaskBaseConfig | @mdf.js

                          Type Alias TaskBaseConfig<Result, Binding>

                          Represents the base configuration for a task

                          -

                          Type Parameters

                          • Result = any
                          • Binding = any
                          diff --git a/docs/types/_mdf_js_tasks.TaskState.html b/docs/types/_mdf_js_tasks.TaskState.html deleted file mode 100644 index da7491bb..00000000 --- a/docs/types/_mdf_js_tasks.TaskState.html +++ /dev/null @@ -1,2 +0,0 @@ -TaskState | @mdf.js

                          Type Alias TaskState

                          TaskState:
                              | CANCELLED
                              | COMPLETED
                              | FAILED
                              | PENDING
                              | RUNNING

                          Represent the state of a task

                          -
                          diff --git a/docs/types/_mdf_js_utils.LoggerFunction.html b/docs/types/_mdf_js_utils.LoggerFunction.html deleted file mode 100644 index 9c7a184e..00000000 --- a/docs/types/_mdf_js_utils.LoggerFunction.html +++ /dev/null @@ -1,3 +0,0 @@ -LoggerFunction | @mdf.js

                          Type Alias LoggerFunction

                          LoggerFunction: ((error: Crash | Multi | Boom) => void)

                          Represents a function that logs errors.

                          -

                          Type declaration

                            • (error): void
                            • Parameters

                              Returns void

                          diff --git a/docs/types/_mdf_js_utils.TaskArguments.html b/docs/types/_mdf_js_utils.TaskArguments.html deleted file mode 100644 index f0289ff8..00000000 --- a/docs/types/_mdf_js_utils.TaskArguments.html +++ /dev/null @@ -1,2 +0,0 @@ -TaskArguments | @mdf.js

                          Type Alias TaskArguments

                          TaskArguments: any[]

                          Type definition for the arguments of a task function.

                          -
                          diff --git a/docs/types/_mdf_js_utils.TaskAsPromise.html b/docs/types/_mdf_js_utils.TaskAsPromise.html deleted file mode 100644 index 5eeeb817..00000000 --- a/docs/types/_mdf_js_utils.TaskAsPromise.html +++ /dev/null @@ -1,3 +0,0 @@ -TaskAsPromise | @mdf.js

                          Type Alias TaskAsPromise<R>

                          TaskAsPromise<R>: ((...args: TaskArguments) => Promise<R>)

                          Represents a task that returns a promise.

                          -

                          Type Parameters

                          • R

                            The type of the result returned by the task.

                            -
                          diff --git a/docs/variables/_mdf.js_amqp-provider.Receiver.Factory.html b/docs/variables/_mdf.js_amqp-provider.Receiver.Factory.html new file mode 100644 index 00000000..828e5aae --- /dev/null +++ b/docs/variables/_mdf.js_amqp-provider.Receiver.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Receiver, ConnectionOptions, Receiver.Port> = ...
                          diff --git a/docs/variables/_mdf.js_amqp-provider.Sender.Factory.html b/docs/variables/_mdf.js_amqp-provider.Sender.Factory.html new file mode 100644 index 00000000..f72a5f5b --- /dev/null +++ b/docs/variables/_mdf.js_amqp-provider.Sender.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<AwaitableSender, ConnectionOptions, Sender.Port> = ...
                          diff --git a/docs/variables/_mdf.js_core.Health.STATUSES.html b/docs/variables/_mdf.js_core.Health.STATUSES.html new file mode 100644 index 00000000..facc3ab0 --- /dev/null +++ b/docs/variables/_mdf.js_core.Health.STATUSES.html @@ -0,0 +1,14 @@ +STATUSES | @mdf.js

                          Variable STATUSESConst

                          STATUSES: readonly ["pass", "fail", "warn"] = ...

                          Indicates whether the service status is acceptable or not. API publishers SHOULD use following +values for the field. +The value of the status field is tightly related with the HTTP response code returned by the +health endpoint. For “pass” and “warn” statuses HTTP response code in the 2xx-3xx range MUST be +used. For “fail” status HTTP response code in the 4xx-5xx range MUST be used. In case of the +“warn” status, endpoint SHOULD return HTTP status in the 2xx-3xx range and additional information +SHOULD be provided, utilizing optional fields of the response. +A health endpoint is only meaningful in the context of the component it indicates the health of. +It has no other meaning or purpose. As such, its health is a conduit to the health of the +component. Clients SHOULD assume that the HTTP response code returned by the health endpoint is +applicable to the entire component (e.g. a larger API or a microservice). This is compatible with +the behavior that current infrastructural tooling expects: load-balancers, service discoveries +and others, utilizing health-checks.

                          +
                          diff --git a/docs/variables/_mdf.js_core.Layer.Provider.PROVIDER_STATES.html b/docs/variables/_mdf.js_core.Layer.Provider.PROVIDER_STATES.html new file mode 100644 index 00000000..46855487 --- /dev/null +++ b/docs/variables/_mdf.js_core.Layer.Provider.PROVIDER_STATES.html @@ -0,0 +1,2 @@ +PROVIDER_STATES | @mdf.js
                          PROVIDER_STATES: readonly ["running", "stopped", "error"] = ...

                          Provider states

                          +
                          diff --git a/docs/variables/_mdf.js_elastic-provider.Elastic.Factory.html b/docs/variables/_mdf.js_elastic-provider.Elastic.Factory.html new file mode 100644 index 00000000..0e323ed3 --- /dev/null +++ b/docs/variables/_mdf.js_elastic-provider.Elastic.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Client, ClientOptions, Elastic.Port> = ...
                          diff --git a/docs/variables/_mdf.js_http-client-provider.HTTP.Factory.html b/docs/variables/_mdf.js_http-client-provider.HTTP.Factory.html new file mode 100644 index 00000000..bfa5d330 --- /dev/null +++ b/docs/variables/_mdf.js_http-client-provider.HTTP.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<AxiosInstance, HTTP.Config, HTTP.Port> = ...
                          diff --git a/docs/variables/_mdf.js_http-server-provider.HTTP.Factory.html b/docs/variables/_mdf.js_http-server-provider.HTTP.Factory.html new file mode 100644 index 00000000..7b02d098 --- /dev/null +++ b/docs/variables/_mdf.js_http-server-provider.HTTP.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<
                              Server<typeof IncomingMessage, typeof ServerResponse>,
                              HTTP.Config,
                              HTTP.Port,
                          > = ...
                          diff --git a/docs/variables/_mdf.js_jsonl-archiver.JSONLArchiver.Factory.html b/docs/variables/_mdf.js_jsonl-archiver.JSONLArchiver.Factory.html new file mode 100644 index 00000000..f6833491 --- /dev/null +++ b/docs/variables/_mdf.js_jsonl-archiver.JSONLArchiver.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<
                              ArchiverManager,
                              Partial<ArchiveOptions>,
                              JSONLArchiver.Port,
                          > = ...
                          diff --git a/docs/variables/_mdf.js_kafka-provider.Consumer.Factory.html b/docs/variables/_mdf.js_kafka-provider.Consumer.Factory.html new file mode 100644 index 00000000..211b8438 --- /dev/null +++ b/docs/variables/_mdf.js_kafka-provider.Consumer.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          diff --git a/docs/variables/_mdf.js_kafka-provider.Producer.Factory.html b/docs/variables/_mdf.js_kafka-provider.Producer.Factory.html new file mode 100644 index 00000000..5b25b903 --- /dev/null +++ b/docs/variables/_mdf.js_kafka-provider.Producer.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          diff --git a/docs/variables/_mdf.js_logger.LOG_LEVELS.html b/docs/variables/_mdf.js_logger.LOG_LEVELS.html new file mode 100644 index 00000000..035989de --- /dev/null +++ b/docs/variables/_mdf.js_logger.LOG_LEVELS.html @@ -0,0 +1,4 @@ +LOG_LEVELS | @mdf.js

                          Variable LOG_LEVELSConst

                          LOG_LEVELS: string[] = ...

                          Copyright 2024 Mytra Control S.L. All rights reserved.

                          +

                          Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +or at https://opensource.org/licenses/MIT.

                          +
                          diff --git a/docs/variables/_mdf.js_logger.default.html b/docs/variables/_mdf.js_logger.default.html new file mode 100644 index 00000000..8495546c --- /dev/null +++ b/docs/variables/_mdf.js_logger.default.html @@ -0,0 +1 @@ +default | @mdf.js

                          Variable defaultConst

                          default: Logger = ...
                          diff --git a/docs/variables/_mdf.js_middlewares.Middleware.html b/docs/variables/_mdf.js_middlewares.Middleware.html new file mode 100644 index 00000000..b1b41bde --- /dev/null +++ b/docs/variables/_mdf.js_middlewares.Middleware.html @@ -0,0 +1 @@ +Middleware | @mdf.js

                          Variable MiddlewareConst

                          Middleware: {
                              Audit: typeof Audit;
                              AuthZ: typeof AuthZ;
                              BodyParser: typeof BodyParser;
                              Cache: typeof Cache;
                              Cors: typeof Cors;
                              Default: typeof Default;
                              ErrorHandler: typeof ErrorHandler;
                              Logger: typeof Logger;
                              Metrics: typeof Metrics;
                              Multer: typeof Multer;
                              NoCache: typeof NoCache;
                              RateLimiter: typeof RateLimiter;
                              RequestId: typeof RequestId;
                              Security: typeof Security;
                          } = ...

                          Type declaration

                          diff --git a/docs/variables/_mdf.js_mongo-provider.Mongo.Factory.html b/docs/variables/_mdf.js_mongo-provider.Mongo.Factory.html new file mode 100644 index 00000000..ae84ea9d --- /dev/null +++ b/docs/variables/_mdf.js_mongo-provider.Mongo.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<MongoClient, Mongo.Config, Mongo.Port> = ...
                          diff --git a/docs/variables/_mdf.js_mqtt-provider.MQTT.Factory.html b/docs/variables/_mdf.js_mqtt-provider.MQTT.Factory.html new file mode 100644 index 00000000..4a0ae539 --- /dev/null +++ b/docs/variables/_mdf.js_mqtt-provider.MQTT.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<MqttClient, MQTT.Config, MQTT.Port> = ...
                          diff --git a/docs/variables/_mdf.js_openc2-core.Control.ACTION_TYPES.html b/docs/variables/_mdf.js_openc2-core.Control.ACTION_TYPES.html new file mode 100644 index 00000000..5c084662 --- /dev/null +++ b/docs/variables/_mdf.js_openc2-core.Control.ACTION_TYPES.html @@ -0,0 +1 @@ +ACTION_TYPES | @mdf.js
                          ACTION_TYPES: ActionType[] = ...
                          diff --git a/docs/variables/_mdf.js_redis-provider.Redis.Factory.html b/docs/variables/_mdf.js_redis-provider.Redis.Factory.html new file mode 100644 index 00000000..ba52c604 --- /dev/null +++ b/docs/variables/_mdf.js_redis-provider.Redis.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          diff --git a/docs/variables/_mdf.js_s3-provider.S3.Factory.html b/docs/variables/_mdf.js_s3-provider.S3.Factory.html new file mode 100644 index 00000000..e0a6d57c --- /dev/null +++ b/docs/variables/_mdf.js_s3-provider.S3.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<S3Client, S3ClientConfig, S3.Port> = ...
                          diff --git a/docs/variables/_mdf.js_service-setup-provider.Setup.Factory.html b/docs/variables/_mdf.js_service-setup-provider.Setup.Factory.html new file mode 100644 index 00000000..fdbd34e1 --- /dev/null +++ b/docs/variables/_mdf.js_service-setup-provider.Setup.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<
                              ConfigManager<Record<string, any>>,
                              Setup.Config<Record<string, any>>,
                              Setup.Port<Record<string, any>>,
                          > = ...
                          diff --git a/docs/variables/_mdf.js_socket-client-provider.SocketIOClient.Factory.html b/docs/variables/_mdf.js_socket-client-provider.SocketIOClient.Factory.html new file mode 100644 index 00000000..eb75d6a1 --- /dev/null +++ b/docs/variables/_mdf.js_socket-client-provider.SocketIOClient.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<
                              Socket<DefaultEventsMap, DefaultEventsMap>,
                              SocketIOClient.Config,
                              SocketIOClient.Port,
                          > = ...
                          diff --git a/docs/variables/_mdf.js_socket-server-provider.SocketIOServer.Factory.html b/docs/variables/_mdf.js_socket-server-provider.SocketIOServer.Factory.html new file mode 100644 index 00000000..0564bd99 --- /dev/null +++ b/docs/variables/_mdf.js_socket-server-provider.SocketIOServer.Factory.html @@ -0,0 +1 @@ +Factory | @mdf.js
                          Factory: Layer.Provider.Factory<
                              Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
                              SocketIOServer.Config,
                              SocketIOServer.Port,
                          > = ...
                          diff --git a/docs/variables/_mdf.js_tasks.STRATEGIES.html b/docs/variables/_mdf.js_tasks.STRATEGIES.html new file mode 100644 index 00000000..86672765 --- /dev/null +++ b/docs/variables/_mdf.js_tasks.STRATEGIES.html @@ -0,0 +1,2 @@ +STRATEGIES | @mdf.js

                          Variable STRATEGIESConst

                          STRATEGIES: STRATEGY[] = ...

                          Array of all the strategy values

                          +
                          diff --git a/docs/variables/_mdf.js_tasks.TASK_STATES.html b/docs/variables/_mdf.js_tasks.TASK_STATES.html new file mode 100644 index 00000000..8cf93052 --- /dev/null +++ b/docs/variables/_mdf.js_tasks.TASK_STATES.html @@ -0,0 +1,2 @@ +TASK_STATES | @mdf.js

                          Variable TASK_STATESConst

                          TASK_STATES: TaskState[] = ...

                          List of all possible task states

                          +
                          diff --git a/docs/variables/_mdf.js_utils.MAX_WAIT_TIME.html b/docs/variables/_mdf.js_utils.MAX_WAIT_TIME.html new file mode 100644 index 00000000..4a7cd736 --- /dev/null +++ b/docs/variables/_mdf.js_utils.MAX_WAIT_TIME.html @@ -0,0 +1,2 @@ +MAX_WAIT_TIME | @mdf.js

                          Variable MAX_WAIT_TIMEConst

                          MAX_WAIT_TIME: 15000

                          Maximum wait time in milliseconds for retrying an operation.

                          +
                          diff --git a/docs/variables/_mdf.js_utils.WAIT_TIME.html b/docs/variables/_mdf.js_utils.WAIT_TIME.html new file mode 100644 index 00000000..79935978 --- /dev/null +++ b/docs/variables/_mdf.js_utils.WAIT_TIME.html @@ -0,0 +1,2 @@ +WAIT_TIME | @mdf.js

                          Variable WAIT_TIMEConst

                          WAIT_TIME: 100

                          The wait time in milliseconds for retrying an operation.

                          +
                          diff --git a/docs/variables/_mdf_js_amqp_provider.Receiver.Factory.html b/docs/variables/_mdf_js_amqp_provider.Receiver.Factory.html deleted file mode 100644 index 95af0e67..00000000 --- a/docs/variables/_mdf_js_amqp_provider.Receiver.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Receiver, ConnectionOptions, Port> = ...
                          diff --git a/docs/variables/_mdf_js_amqp_provider.Sender.Factory.html b/docs/variables/_mdf_js_amqp_provider.Sender.Factory.html deleted file mode 100644 index 25f501a4..00000000 --- a/docs/variables/_mdf_js_amqp_provider.Sender.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<AwaitableSender, ConnectionOptions, Port> = ...
                          diff --git a/docs/variables/_mdf_js_core.Health.STATUSES.html b/docs/variables/_mdf_js_core.Health.STATUSES.html deleted file mode 100644 index 246d5e91..00000000 --- a/docs/variables/_mdf_js_core.Health.STATUSES.html +++ /dev/null @@ -1,14 +0,0 @@ -STATUSES | @mdf.js

                          Variable STATUSESConst

                          STATUSES: readonly ["pass", "fail", "warn"] = ...

                          Indicates whether the service status is acceptable or not. API publishers SHOULD use following -values for the field. -The value of the status field is tightly related with the HTTP response code returned by the -health endpoint. For “pass” and “warn” statuses HTTP response code in the 2xx-3xx range MUST be -used. For “fail” status HTTP response code in the 4xx-5xx range MUST be used. In case of the -“warn” status, endpoint SHOULD return HTTP status in the 2xx-3xx range and additional information -SHOULD be provided, utilizing optional fields of the response. -A health endpoint is only meaningful in the context of the component it indicates the health of. -It has no other meaning or purpose. As such, its health is a conduit to the health of the -component. Clients SHOULD assume that the HTTP response code returned by the health endpoint is -applicable to the entire component (e.g. a larger API or a microservice). This is compatible with -the behavior that current infrastructural tooling expects: load-balancers, service discoveries -and others, utilizing health-checks.

                          -
                          diff --git a/docs/variables/_mdf_js_core.Layer.Provider.PROVIDER_STATES.html b/docs/variables/_mdf_js_core.Layer.Provider.PROVIDER_STATES.html deleted file mode 100644 index eabde0a6..00000000 --- a/docs/variables/_mdf_js_core.Layer.Provider.PROVIDER_STATES.html +++ /dev/null @@ -1,2 +0,0 @@ -PROVIDER_STATES | @mdf.js
                          PROVIDER_STATES: readonly ["running", "stopped", "error"] = ...

                          Provider states

                          -
                          diff --git a/docs/variables/_mdf_js_elastic_provider.Elastic.Factory.html b/docs/variables/_mdf_js_elastic_provider.Elastic.Factory.html deleted file mode 100644 index 3d553af3..00000000 --- a/docs/variables/_mdf_js_elastic_provider.Elastic.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Client, ClientOptions, Elastic.Port> = ...
                          diff --git a/docs/variables/_mdf_js_http_client_provider.HTTP.Factory.html b/docs/variables/_mdf_js_http_client_provider.HTTP.Factory.html deleted file mode 100644 index db4cf0c8..00000000 --- a/docs/variables/_mdf_js_http_client_provider.HTTP.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<AxiosInstance, HTTP.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_http_server_provider.HTTP.Factory.html b/docs/variables/_mdf_js_http_server_provider.HTTP.Factory.html deleted file mode 100644 index c1622b6c..00000000 --- a/docs/variables/_mdf_js_http_server_provider.HTTP.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Server<typeof IncomingMessage, typeof ServerResponse>, HTTP.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_jsonl_archiver.JSONLArchiver.Factory.html b/docs/variables/_mdf_js_jsonl_archiver.JSONLArchiver.Factory.html deleted file mode 100644 index 08833a4e..00000000 --- a/docs/variables/_mdf_js_jsonl_archiver.JSONLArchiver.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          diff --git a/docs/variables/_mdf_js_kafka_provider.Consumer.Factory.html b/docs/variables/_mdf_js_kafka_provider.Consumer.Factory.html deleted file mode 100644 index 28717a43..00000000 --- a/docs/variables/_mdf_js_kafka_provider.Consumer.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Consumer, Consumer.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_kafka_provider.Producer.Factory.html b/docs/variables/_mdf_js_kafka_provider.Producer.Factory.html deleted file mode 100644 index f06ed58c..00000000 --- a/docs/variables/_mdf_js_kafka_provider.Producer.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Producer, Producer.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_logger.default.html b/docs/variables/_mdf_js_logger.default.html deleted file mode 100644 index 3b43f37d..00000000 --- a/docs/variables/_mdf_js_logger.default.html +++ /dev/null @@ -1 +0,0 @@ -default | @mdf.js

                          Variable defaultConst

                          default: Logger = ...
                          diff --git a/docs/variables/_mdf_js_middlewares.Middleware.html b/docs/variables/_mdf_js_middlewares.Middleware.html deleted file mode 100644 index 1203b83a..00000000 --- a/docs/variables/_mdf_js_middlewares.Middleware.html +++ /dev/null @@ -1 +0,0 @@ -Middleware | @mdf.js

                          Variable MiddlewareConst

                          Middleware: {
                              Audit: typeof Audit;
                              AuthZ: typeof AuthZ;
                              BodyParser: typeof BodyParser;
                              Cache: typeof Cache;
                              Cors: typeof Cors;
                              Default: typeof Default;
                              ErrorHandler: typeof ErrorHandler;
                              Logger: typeof Logger;
                              Metrics: typeof Metrics;
                              Multer: typeof Multer;
                              NoCache: typeof NoCache;
                              RateLimiter: typeof RateLimiter;
                              RequestId: typeof RequestId;
                              Security: typeof Security;
                          } = ...
                          diff --git a/docs/variables/_mdf_js_mongo_provider.Mongo.Factory.html b/docs/variables/_mdf_js_mongo_provider.Mongo.Factory.html deleted file mode 100644 index 1f568fad..00000000 --- a/docs/variables/_mdf_js_mongo_provider.Mongo.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<MongoClient, Mongo.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_mqtt_provider.MQTT.Factory.html b/docs/variables/_mdf_js_mqtt_provider.MQTT.Factory.html deleted file mode 100644 index 0157f008..00000000 --- a/docs/variables/_mdf_js_mqtt_provider.MQTT.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<MqttClient, MQTT.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_openc2_core.Control.ACTION_TYPES.html b/docs/variables/_mdf_js_openc2_core.Control.ACTION_TYPES.html deleted file mode 100644 index 52eb8d8f..00000000 --- a/docs/variables/_mdf_js_openc2_core.Control.ACTION_TYPES.html +++ /dev/null @@ -1 +0,0 @@ -ACTION_TYPES | @mdf.js
                          ACTION_TYPES: ActionType[] = ...
                          diff --git a/docs/variables/_mdf_js_redis_provider.Redis.Factory.html b/docs/variables/_mdf_js_redis_provider.Redis.Factory.html deleted file mode 100644 index 7c907ed4..00000000 --- a/docs/variables/_mdf_js_redis_provider.Redis.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Redis, Redis.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_s3_provider.S3.Factory.html b/docs/variables/_mdf_js_s3_provider.S3.Factory.html deleted file mode 100644 index c8cfbfc0..00000000 --- a/docs/variables/_mdf_js_s3_provider.S3.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<S3Client, S3ClientConfig, Port> = ...
                          diff --git a/docs/variables/_mdf_js_service_setup_provider.Setup.Factory.html b/docs/variables/_mdf_js_service_setup_provider.Setup.Factory.html deleted file mode 100644 index e39e4262..00000000 --- a/docs/variables/_mdf_js_service_setup_provider.Setup.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<ConfigManager<Record<string, any>>, Setup.Config<Record<string, any>>, Port<Record<string, any>>> = ...
                          diff --git a/docs/variables/_mdf_js_socket_client_provider.SocketIOClient.Factory.html b/docs/variables/_mdf_js_socket_client_provider.SocketIOClient.Factory.html deleted file mode 100644 index 83b9cf14..00000000 --- a/docs/variables/_mdf_js_socket_client_provider.SocketIOClient.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Socket<DefaultEventsMap, DefaultEventsMap>, SocketIOClient.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_socket_server_provider.SocketIOServer.Factory.html b/docs/variables/_mdf_js_socket_server_provider.SocketIOServer.Factory.html deleted file mode 100644 index cedbe349..00000000 --- a/docs/variables/_mdf_js_socket_server_provider.SocketIOServer.Factory.html +++ /dev/null @@ -1 +0,0 @@ -Factory | @mdf.js
                          Factory: Layer.Provider.Factory<Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>, SocketIOServer.Config, Port> = ...
                          diff --git a/docs/variables/_mdf_js_tasks.STRATEGIES.html b/docs/variables/_mdf_js_tasks.STRATEGIES.html deleted file mode 100644 index ce521b43..00000000 --- a/docs/variables/_mdf_js_tasks.STRATEGIES.html +++ /dev/null @@ -1,2 +0,0 @@ -STRATEGIES | @mdf.js

                          Variable STRATEGIESConst

                          STRATEGIES: STRATEGY[] = ...

                          Array of all the strategy values

                          -
                          diff --git a/docs/variables/_mdf_js_tasks.TASK_STATES.html b/docs/variables/_mdf_js_tasks.TASK_STATES.html deleted file mode 100644 index 14e058ae..00000000 --- a/docs/variables/_mdf_js_tasks.TASK_STATES.html +++ /dev/null @@ -1,2 +0,0 @@ -TASK_STATES | @mdf.js

                          Variable TASK_STATESConst

                          TASK_STATES: TaskState[] = ...

                          List of all possible task states

                          -
                          diff --git a/docs/variables/_mdf_js_utils.MAX_WAIT_TIME.html b/docs/variables/_mdf_js_utils.MAX_WAIT_TIME.html deleted file mode 100644 index 6c31c12f..00000000 --- a/docs/variables/_mdf_js_utils.MAX_WAIT_TIME.html +++ /dev/null @@ -1,2 +0,0 @@ -MAX_WAIT_TIME | @mdf.js

                          Variable MAX_WAIT_TIMEConst

                          MAX_WAIT_TIME: 15000 = 15000

                          Maximum wait time in milliseconds for retrying an operation.

                          -
                          diff --git a/docs/variables/_mdf_js_utils.WAIT_TIME.html b/docs/variables/_mdf_js_utils.WAIT_TIME.html deleted file mode 100644 index efcbaf06..00000000 --- a/docs/variables/_mdf_js_utils.WAIT_TIME.html +++ /dev/null @@ -1,2 +0,0 @@ -WAIT_TIME | @mdf.js

                          Variable WAIT_TIMEConst

                          WAIT_TIME: 100 = 100

                          The wait time in milliseconds for retrying an operation.

                          -
                          diff --git a/package.json b/package.json index b954966d..82db4c92 100644 --- a/package.json +++ b/package.json @@ -1,82 +1,87 @@ -{ - "name": "mms", - "version": "1.16.0", - "private": true, - "license": "MIT", - "workspaces": [ - "packages/api/*", - "packages/components/*", - "packages/providers/*", - "packages/tools/*" - ], - "scripts": { - "@:snyk:report": "snyk-to-html -i report.json -o report.html -a", - "@:snyk:monitor": "snyk monitor --yarn-workspaces --strict-out-of-sync=false", - "@:snyk:test": "snyk test --yarn-workspaces --strict-out-of-sync=false --json-file-output=report.json --fail-on=all", - "@:snyk:auth": "snyk auth", - "@:snyk": "yarn run @:snyk:auth && yarn run @:snyk:test && yarn run @:snyk:report && yarn run @:snyk:monitor", - "snyk": "yarn run @:snyk:test && yarn run @:snyk:report && yarn run @:snyk:monitor", - "@:clean:dist": "find . -name \"dist\" -type d -prune -exec rm -rf {} +", - "@:clean:node_modules": "find . -name \"node_modules\" -type d -prune -exec rm -rf {} +", - "@:clean:build": "rimraf -g \"./packages/*/*/*+(.tsbuildinfo)\" \"./{coverage,docs,mutations,tmp}\" \"./packages/*/*/{.turbo,dist,logs,reports}\"", - "clean": "yarn run @:clean:build && yarn run @:clean:node_modules && rm yarn.lock", - "@:docs:media": ".config/copyMedia.sh", - "@:docs:typedoc": "typedoc --options typedoc.json", - "@:docs:env": "turbo run envDoc --force", - "docs": "yarn run @:docs:env && yarn run @:docs:typedoc && yarn run @:docs:media", - "build": "turbo run build", - "compile": "tsc --build --clean && tsc --build", - "dev": "turbo run dev", - "test": "turbo run test licenses --concurrency=1", - "check-dependencies": "turbo run check-dependencies --continue --concurrency=1", - "lint": "turbo run lint", - "prettier": "prettier --config .prettierrc.js --write **/src/**/*.ts", - "sort": "sort-package-json \"packages/*/*/package.json\" package.json" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@stryker-mutator/core": "^8.6.0", - "@stryker-mutator/jest-runner": "^8.6.0", - "@stryker-mutator/typescript-checker": "^8.6.0", - "@types/jest": "29.5.13", - "@types/node": "22.7.5", - "@typescript-eslint/eslint-plugin": "8.8.1", - "@typescript-eslint/parser": "8.8.1", - "cross-env": "^7.0.3", - "dependency-cruiser": "^16.4.2", - "eslint": "^9.12.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-tsdoc": "^0.3.0", - "glob": "^11.0.0", - "jest": "29.7.0", - "jest-environment-jsdom": "29.7.0", - "jest-html-reporter": "^3.10.1", - "jest-html-reporters": "^3.1.7", - "jest-junit": "^16.0.0", - "jest-slow-test-reporter": "^1.0.0", - "lerna": "^8.1.6", - "license-checker": "^25.0.1", - "npm-check": "^6.0.1", - "prettier": "^3.3.3", - "remark": "^15.0.1", - "remark-gfm": "^4.0.0", - "remark-lint": "^10.0.0", - "remark-lint-unordered-list-marker-style": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "remark-toc": "^9.0.0", - "rimraf": "^6.0.1", - "snyk": "^1.1293.0", - "snyk-to-html": "^2.5.1", - "sort-package-json": "^2.10.1", - "ts-jest": "29.2.5", - "ts-node": "10.9.2", - "turbo": "^2.1.3", - "typedoc": "0.26.8", - "typedoc-plugin-missing-exports": "^3.0.0", - "typescript": "5.6.2" - }, - "packageManager": "yarn@1.22.19" -} +{ + "name": "mms", + "version": "1.16.0", + "private": true, + "license": "MIT", + "workspaces": [ + "packages/api/*", + "packages/components/*", + "packages/providers/*", + "packages/tools/*" + ], + "scripts": { + "@:clean:build": "rimraf -g \"./packages/*/*/*+(.tsbuildinfo)\" \"./{coverage,docs,mutations,tmp}\" \"./packages/*/*/{.turbo,dist,logs,reports}\"", + "@:clean:dist": "find . -name \"dist\" -type d -prune -exec rm -rf {} +", + "@:clean:node_modules": "find . -name \"node_modules\" -type d -prune -exec rm -rf {} +", + "@:docs:env": "turbo run envDoc --force", + "@:docs:media": ".config/copyMedia.sh", + "@:docs:typedoc": "typedoc --options typedoc.json", + "@:snyk": "yarn run @:snyk:auth && yarn run @:snyk:test && yarn run @:snyk:report && yarn run @:snyk:monitor", + "@:snyk:auth": "snyk auth", + "@:snyk:monitor": "snyk monitor --yarn-workspaces --strict-out-of-sync=false", + "@:snyk:report": "snyk-to-html -i report.json -o report.html -a", + "@:snyk:test": "snyk test --yarn-workspaces --strict-out-of-sync=false --json-file-output=report.json --fail-on=all", + "build": "turbo run build", + "check-dependencies": "turbo run check-dependencies --continue --concurrency=1", + "clean": "yarn run @:clean:build && yarn run @:clean:node_modules && rm yarn.lock", + "compile": "tsc --build --clean && tsc --build", + "dev": "turbo run dev", + "docs": "yarn run @:docs:env && yarn run @:docs:typedoc && yarn run @:docs:media", + "lint": "turbo run lint", + "prettier": "prettier --config .prettierrc.js --write **/src/**/*.ts", + "snyk": "yarn run @:snyk:test && yarn run @:snyk:report && yarn run @:snyk:monitor", + "sort": "sort-package-json \"packages/*/*/package.json\" package.json", + "test": "turbo run test licenses --concurrency=1" + }, + "resolutions": { + "@types/express": "4.17.21", + "@types/express-serve-static-core": "4.19.6" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@stryker-mutator/core": "^8.7.1", + "@stryker-mutator/jest-runner": "^8.7.1", + "@stryker-mutator/typescript-checker": "^8.7.1", + "@types/jest": "29.5.14", + "@types/node": "22.10.2", + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", + "cross-env": "^7.0.3", + "dependency-cruiser": "^16.8.0", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-tsdoc": "^0.4.0", + "glob": "^11.0.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "jest-html-reporter": "^3.10.1", + "jest-html-reporters": "^3.1.7", + "jest-junit": "^16.0.0", + "jest-slow-test-reporter": "^1.0.0", + "lerna": "^8.1.9", + "license-checker": "^25.0.1", + "npm-check": "^6.0.1", + "owasp-dependency-check": "^0.0.24", + "prettier": "^3.4.2", + "remark": "^15.0.1", + "remark-gfm": "^4.0.0", + "remark-lint": "^10.0.0", + "remark-lint-unordered-list-marker-style": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "remark-toc": "^9.0.0", + "rimraf": "^6.0.1", + "snyk": "^1.1294.3", + "snyk-to-html": "^2.5.1", + "sort-package-json": "^2.12.0", + "ts-jest": "29.2.5", + "ts-node": "10.9.2", + "turbo": "^2.3.3", + "typedoc": "0.27.5", + "typedoc-plugin-missing-exports": "^3.1.0", + "typescript": "5.7.2" + }, + "packageManager": "yarn@1.22.19" +} diff --git a/packages/api/core/README.md b/packages/api/core/README.md index 13ac9d87..e977d031 100644 --- a/packages/api/core/README.md +++ b/packages/api/core/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/core/package.json b/packages/api/core/package.json index 36a43f99..f978ffcd 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -38,17 +38,16 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "@types/express": "^4.17.21", - "express": "^4.21.1", + "express": "^4.21.2", "joi": "^17.13.3", "lodash": "^4.17.21", "prom-client": "^15.1.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" + "tslib": "^2.8.1", + "uuid": "^11.0.3" }, "devDependencies": { "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" + "@types/lodash": "^4.17.13" }, "engines": { "node": ">=16.14.2" diff --git a/packages/api/core/src/Health/overallStatus.test.ts b/packages/api/core/src/Health/overallStatus.test.ts index 811b94fd..445b9737 100644 --- a/packages/api/core/src/Health/overallStatus.test.ts +++ b/packages/api/core/src/Health/overallStatus.test.ts @@ -1,97 +1,98 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health } from '@mdf.js/core'; -import { overallStatus } from './overallStatus'; - -describe('#overallStatus', () => { - describe('#Happy path', () => { - it(`Should return 'pass' if all checks are 'pass'`, () => { - const checks: Health.Checks = { - 'test:test': [ - { - componentId: 'test:test', - name: 'test', - status: 'pass', - message: 'test', - }, - ], - 'test:test2': [ - { - componentId: 'test:test2', - name: 'test2', - status: 'pass', - message: 'test2', - }, - ], - }; - expect(overallStatus(checks)).toEqual('pass'); - }); - it(`Should return 'fail' if any check is 'fail'`, () => { - const checks: Health.Checks = { - 'test:test': [ - { - componentId: 'test:test', - name: 'test', - status: 'pass', - message: 'test', - }, - ], - 'test:test3': [ - { - componentId: 'test:test31', - name: 'test31', - status: 'warn', - message: 'test31', - }, - { - componentId: 'test:test32', - name: 'test32', - status: 'pass', - message: 'test32', - }, - ], - 'test:test2': [ - { - componentId: 'test:test21', - name: 'test21', - status: 'fail', - message: 'test21', - }, - { - componentId: 'test:test22', - name: 'test22', - status: 'pass', - message: 'test22', - }, - ], - }; - expect(overallStatus(checks)).toEqual('fail'); - }); - it(`Should return 'warn' if any check is 'warn'`, () => { - const checks: Health.Checks = { - 'test:test': [ - { - componentId: 'test:test', - name: 'test', - status: 'pass', - message: 'test', - }, - ], - 'test:test2': [ - { - componentId: 'test:test2', - name: 'test2', - status: 'warn', - message: 'test2', - }, - ], - }; - expect(overallStatus(checks)).toEqual('warn'); - }); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { overallStatus } from './overallStatus'; +import { Checks } from './types'; + +describe('#overallStatus', () => { + describe('#Happy path', () => { + it(`Should return 'pass' if all checks are 'pass'`, () => { + const checks: Checks = { + 'test:test': [ + { + componentId: 'test:test', + name: 'test', + status: 'pass', + message: 'test', + }, + ], + 'test:test2': [ + { + componentId: 'test:test2', + name: 'test2', + status: 'pass', + message: 'test2', + }, + ], + }; + expect(overallStatus(checks)).toEqual('pass'); + }); + it(`Should return 'fail' if any check is 'fail'`, () => { + const checks: Checks = { + 'test:test': [ + { + componentId: 'test:test', + name: 'test', + status: 'pass', + message: 'test', + }, + ], + 'test:test3': [ + { + componentId: 'test:test31', + name: 'test31', + status: 'warn', + message: 'test31', + }, + { + componentId: 'test:test32', + name: 'test32', + status: 'pass', + message: 'test32', + }, + ], + 'test:test2': [ + { + componentId: 'test:test21', + name: 'test21', + status: 'fail', + message: 'test21', + }, + { + componentId: 'test:test22', + name: 'test22', + status: 'pass', + message: 'test22', + }, + ], + }; + expect(overallStatus(checks)).toEqual('fail'); + }); + it(`Should return 'warn' if any check is 'warn'`, () => { + const checks: Checks = { + 'test:test': [ + { + componentId: 'test:test', + name: 'test', + status: 'pass', + message: 'test', + }, + ], + 'test:test2': [ + { + componentId: 'test:test2', + name: 'test2', + status: 'warn', + message: 'test2', + }, + ], + }; + expect(overallStatus(checks)).toEqual('warn'); + }); + }); +}); + diff --git a/packages/api/core/src/Jobs/JobHandler.ts b/packages/api/core/src/Jobs/JobHandler.ts index c27b00a1..7f1ec9d5 100644 --- a/packages/api/core/src/Jobs/JobHandler.ts +++ b/packages/api/core/src/Jobs/JobHandler.ts @@ -1,282 +1,283 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Crash, Multi } from '@mdf.js/crash'; -import { EventEmitter } from 'events'; -import { v4, v5 } from 'uuid'; -import { MDF_NAMESPACE_OID } from '../const'; -import { - AnyHeaders, - AnyOptions, - DoneEventHandler, - JobObject, - JobRequest, - Options, - Result, - Status, -} from './types'; - -/** - * JobHandler class - * @category @mdf.js/core - */ -export declare interface JobHandler { - /** - * Register an event listener over the `done` event, which is emitted when a job has ended, either - * due to completion or failure. - * @param event - `done` event - * @param listener - The listener function to add - */ - on(event: 'done', listener: DoneEventHandler): this; - /** - * Register an event listener over the `done` event, which is emitted when a job has ended, either - * due to completion or failure. - * @param event - `done` event - * @param listener - The listener function to add - */ - addListener(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a event listener over the `done` event, at the beginning of the listeners array, - * which is emitted when a job has ended, either due to completion or failure. - * @param event - `done` event - * @param listener - The listener function to add - */ - prependListener(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, which is emitted when a job has - * ended, either due to completion or failure. - * @param event - `done` event - * @param listener - The listener function to add - */ - once(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, at the beginning of the listeners - * array, which is emitted when a job has ended, either due to completion or failure. - * @param event - `done` event - * @param listener - The listener function to add - */ - prependOnceListener(event: 'done', listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - removeListener(event: 'done', listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - off(event: 'done', listener: DoneEventHandler): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `done` event - */ - removeAllListeners(event?: 'done'): this; -} - -/** - * JobHandler class - * @category @mdf.js/core - * @typeParam Type - Job type, used as selector for strategies in job processors - * @typeParam Data - Job payload - * @typeParam CustomHeaders - Custom headers, used to pass specific information for job processors - * @typeParam CustomOptions - Custom options, used to pass specific information for job processors - */ -export class JobHandler< - Type extends string = string, - Data = unknown, - CustomHeaders extends Record = AnyHeaders, - CustomOptions extends Record = AnyOptions, - > - extends EventEmitter - implements JobObject -{ - /** Unique job processing identification */ - public readonly uuid: string; - /** User job request identifier, defined by the user */ - public readonly jobUserId: string; - /** Unique user job request identification, based on jobUserId */ - public readonly jobUserUUID: string; - /** Date object with the timestamp when the job was created */ - public readonly createdAt: Date; - /** Job type, used as selector for strategies in job processors */ - public readonly type: Type; - /** Job meta information, used to pass specific information for job processors */ - public readonly options?: Options; - /** Date object with the timestamp when the job was resolved */ - private resolvedAt?: Date; - /** Job processing status */ - private _status: Status = Status.PENDING; - /** Error raised during job processing */ - private _errors?: Multi; - /** Job payload */ - private _data: Data; - /** Pending confirmation */ - private pendingDone: number; - /** - * Create a new instance of JobHandler - * @param jobRequest - job request object - */ - constructor(jobRequest: JobRequest); - /** - * Create a new instance of JobHandler - * @param jobUserId - User job request identifier, defined by the user - * @param data - Job payload - * @param type - Job type, used as selector for strategies in job processors - * @param options - JobHandler options - */ - constructor( - jobUserId: string, - data: Data, - type?: Type, - options?: Options - ); - constructor( - jobUserIdOrJobRequest: string | JobRequest, - data?: Data, - type?: Type, - options?: Options - ) { - super(); - this.uuid = v4(); - if (typeof jobUserIdOrJobRequest === 'string') { - this.jobUserId = jobUserIdOrJobRequest; - this.type = type ?? ('default' as Type); - this.options = options; - this._data = data as Data; - } else if (typeof jobUserIdOrJobRequest === 'object') { - this.jobUserId = jobUserIdOrJobRequest.jobUserId; - this.type = jobUserIdOrJobRequest.type ?? ('default' as Type); - this.options = jobUserIdOrJobRequest.options; - this._data = jobUserIdOrJobRequest.data; - } else { - throw new Crash( - `Error creating a valid JobHandler, the first parameter must be a jobUserId or a JobRequest object`, - this.uuid, - { name: 'ValidationError' } - ); - } - if (this._data === undefined || this._data === null) { - throw new Crash('Error creating a valid JobHandler, data is mandatory', this.uuid, { - name: 'ValidationError', - }); - } - if (typeof this.type !== 'string') { - throw new Crash('Error creating a valid JobHandler, type must be a string', this.uuid, { - name: 'ValidationError', - }); - } - if (this.options && typeof this.options !== 'object') { - throw new Crash('Error creating a valid JobHandler, options should be a object', v4(), { - name: 'ValidationError', - }); - } - this.jobUserUUID = v5(this.jobUserId, MDF_NAMESPACE_OID); - this.createdAt = new Date(); - this.pendingDone = options?.numberOfHandlers || 1; - } - /** Job payload */ - public get data(): Data { - this.updateStatusToProcessing(); - return this._data; - } - public set data(value: Data) { - this.updateStatusToProcessing(); - this._data = value; - } - /** True if the job task raised any error */ - public get hasErrors(): boolean { - if (this._errors) { - return true; - } else { - return false; - } - } - /** Errors raised during the job */ - public get errors(): Multi | undefined { - return this._errors; - } - /** Return the process time in msec */ - public get processTime(): number { - if (this.resolvedAt) { - return this.resolvedAt.getTime() - this.createdAt.getTime(); - } else { - return -1; - } - } - /** Return the job processing status */ - public get status(): Status { - return this._status; - } - /** - * Add a new error in the job - * @param error - error to be added to the job - */ - public addError(error: Crash | Multi): void { - if (this._errors) { - this._errors.push(error); - } else { - this._errors = new Multi('Errors in job processing', this.uuid, { - name: 'ValidationError', - causes: error, - }); - } - this.updateStatusToProcessing(); - } - /** - * Notify the results of the process - * @param error - conditional parameter for error notification - */ - public done(error?: Crash): void { - this.pendingDone--; - if (error) { - this.addError(error); - } - if (this.pendingDone <= 0 && !this.resolvedAt) { - this.resolvedAt = new Date(); - if (this.hasErrors) { - this._status = Status.FAILED; - } else { - this._status = Status.COMPLETED; - } - this.emit('done', this.uuid, this.result(), this._errors); - } - } - /** Return the result of the publication process */ - public result(): Result { - return { - uuid: this.uuid, - createdAt: this.createdAt.toISOString(), - resolvedAt: this.resolvedAt?.toISOString() || '', - quantity: Array.isArray(this._data) ? this._data.length : 1, - hasErrors: this._errors ? this._errors.size > 0 : false, - errors: this._errors ? this._errors.toJSON() : undefined, - jobUserId: this.jobUserId, - jobUserUUID: this.jobUserUUID, - type: this.type, - status: this._status, - }; - } - /** Return an object with the key information of the job, this information is used by the plugs */ - public toObject(): JobObject { - return { - uuid: this.uuid, - data: this.data, - type: this.type, - jobUserId: this.jobUserId, - jobUserUUID: this.jobUserUUID, - status: this._status, - options: this.options, - }; - } - /** Update the job status to processing if it is in pending state */ - private updateStatusToProcessing(): void { - if (this._status === Status.PENDING) { - this._status = Status.PROCESSING; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Crash, Multi } from '@mdf.js/crash'; +import { EventEmitter } from 'events'; +import { v4, v5 } from 'uuid'; +import { MDF_NAMESPACE_OID } from '../const'; +import { + AnyHeaders, + AnyOptions, + DoneEventHandler, + JobObject, + JobRequest, + Options, + Result, + Status, +} from './types'; + +/** + * JobHandler class + * @category @mdf.js/core + */ +export declare interface JobHandler { + /** + * Register an event listener over the `done` event, which is emitted when a job has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - The listener function to add + */ + on(event: 'done', listener: DoneEventHandler): this; + /** + * Register an event listener over the `done` event, which is emitted when a job has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - The listener function to add + */ + addListener(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a event listener over the `done` event, at the beginning of the listeners array, + * which is emitted when a job has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - The listener function to add + */ + prependListener(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, which is emitted when a job has + * ended, either due to completion or failure. + * @param event - `done` event + * @param listener - The listener function to add + */ + once(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, at the beginning of the listeners + * array, which is emitted when a job has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - The listener function to add + */ + prependOnceListener(event: 'done', listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - The listener function to remove + */ + removeListener(event: 'done', listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - The listener function to remove + */ + off(event: 'done', listener: DoneEventHandler): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `done` event + */ + removeAllListeners(event?: 'done'): this; +} + +/** + * JobHandler class + * @category @mdf.js/core + * @typeParam Type - Job type, used as selector for strategies in job processors + * @typeParam Data - Job payload + * @typeParam CustomHeaders - Custom headers, used to pass specific information for job processors + * @typeParam CustomOptions - Custom options, used to pass specific information for job processors + */ +export class JobHandler< + Type extends string = string, + Data = unknown, + CustomHeaders extends Record = AnyHeaders, + CustomOptions extends Record = AnyOptions, + > + extends EventEmitter + implements JobObject +{ + /** Unique job processing identification */ + public readonly uuid: string; + /** User job request identifier, defined by the user */ + public readonly jobUserId: string; + /** Unique user job request identification, based on jobUserId */ + public readonly jobUserUUID: string; + /** Date object with the timestamp when the job was created */ + public readonly createdAt: Date; + /** Job type, used as selector for strategies in job processors */ + public readonly type: Type; + /** Job meta information, used to pass specific information for job processors */ + public readonly options?: Options; + /** Date object with the timestamp when the job was resolved */ + private resolvedAt?: Date; + /** Job processing status */ + private _status: Status = Status.PENDING; + /** Error raised during job processing */ + private _errors?: Multi; + /** Job payload */ + private _data: Data; + /** Pending confirmation */ + private pendingDone: number; + /** + * Create a new instance of JobHandler + * @param jobRequest - job request object + */ + constructor(jobRequest: JobRequest); + /** + * Create a new instance of JobHandler + * @param jobUserId - User job request identifier, defined by the user + * @param data - Job payload + * @param type - Job type, used as selector for strategies in job processors + * @param options - JobHandler options + */ + constructor( + jobUserId: string, + data: Data, + type?: Type, + options?: Options + ); + constructor( + jobUserIdOrJobRequest: string | JobRequest, + data?: Data, + type?: Type, + options?: Options + ) { + super(); + this.uuid = v4(); + if (typeof jobUserIdOrJobRequest === 'string') { + this.jobUserId = jobUserIdOrJobRequest; + this.type = type ?? ('default' as Type); + this.options = options; + this._data = data as Data; + } else if (typeof jobUserIdOrJobRequest === 'object') { + this.jobUserId = jobUserIdOrJobRequest.jobUserId; + this.type = jobUserIdOrJobRequest.type ?? ('default' as Type); + this.options = jobUserIdOrJobRequest.options; + this._data = jobUserIdOrJobRequest.data; + } else { + throw new Crash( + `Error creating a valid JobHandler, the first parameter must be a jobUserId or a JobRequest object`, + this.uuid, + { name: 'ValidationError' } + ); + } + if (this._data === undefined || this._data === null) { + throw new Crash('Error creating a valid JobHandler, data is mandatory', this.uuid, { + name: 'ValidationError', + }); + } + if (typeof this.type !== 'string') { + throw new Crash('Error creating a valid JobHandler, type must be a string', this.uuid, { + name: 'ValidationError', + }); + } + if (this.options && typeof this.options !== 'object') { + throw new Crash('Error creating a valid JobHandler, options should be a object', v4(), { + name: 'ValidationError', + }); + } + this.jobUserUUID = v5(this.jobUserId, MDF_NAMESPACE_OID); + this.createdAt = new Date(); + this.pendingDone = options?.numberOfHandlers ?? 1; + } + /** Job payload */ + public get data(): Data { + this.updateStatusToProcessing(); + return this._data; + } + public set data(value: Data) { + this.updateStatusToProcessing(); + this._data = value; + } + /** True if the job task raised any error */ + public get hasErrors(): boolean { + if (this._errors) { + return true; + } else { + return false; + } + } + /** Errors raised during the job */ + public get errors(): Multi | undefined { + return this._errors; + } + /** Return the process time in msec */ + public get processTime(): number { + if (this.resolvedAt) { + return this.resolvedAt.getTime() - this.createdAt.getTime(); + } else { + return -1; + } + } + /** Return the job processing status */ + public get status(): Status { + return this._status; + } + /** + * Add a new error in the job + * @param error - error to be added to the job + */ + public addError(error: Crash | Multi): void { + if (this._errors) { + this._errors.push(error); + } else { + this._errors = new Multi('Errors in job processing', this.uuid, { + name: 'ValidationError', + causes: error, + }); + } + this.updateStatusToProcessing(); + } + /** + * Notify the results of the process + * @param error - conditional parameter for error notification + */ + public done(error?: Crash): void { + this.pendingDone--; + if (error) { + this.addError(error); + } + if (this.pendingDone <= 0 && !this.resolvedAt) { + this.resolvedAt = new Date(); + if (this.hasErrors) { + this._status = Status.FAILED; + } else { + this._status = Status.COMPLETED; + } + this.emit('done', this.uuid, this.result(), this._errors); + } + } + /** Return the result of the publication process */ + public result(): Result { + return { + uuid: this.uuid, + createdAt: this.createdAt.toISOString(), + resolvedAt: this.resolvedAt?.toISOString() ?? '', + quantity: Array.isArray(this._data) ? this._data.length : 1, + hasErrors: this._errors ? this._errors.size > 0 : false, + errors: this._errors ? this._errors.toJSON() : undefined, + jobUserId: this.jobUserId, + jobUserUUID: this.jobUserUUID, + type: this.type, + status: this._status, + }; + } + /** Return an object with the key information of the job, this information is used by the plugs */ + public toObject(): JobObject { + return { + uuid: this.uuid, + data: this.data, + type: this.type, + jobUserId: this.jobUserId, + jobUserUUID: this.jobUserUUID, + status: this._status, + options: this.options, + }; + } + /** Update the job status to processing if it is in pending state */ + private updateStatusToProcessing(): void { + if (this._status === Status.PENDING) { + this._status = Status.PROCESSING; + } + } +} + diff --git a/packages/api/core/src/Jobs/types/Strategy.i.ts b/packages/api/core/src/Jobs/types/Strategy.i.ts index 86f1b14a..6f8e77ac 100644 --- a/packages/api/core/src/Jobs/types/Strategy.i.ts +++ b/packages/api/core/src/Jobs/types/Strategy.i.ts @@ -1,28 +1,28 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { AnyHeaders } from './Headers.i'; -import { JobObject } from './JobObject.i'; -import { AnyOptions } from './Options.i'; - -/** Base class for strategies */ -export interface Strategy< - Type extends string = string, - Data = any, - CustomHeaders extends Record = AnyHeaders, - CustomOptions extends Record = AnyOptions, -> { - /** Strategy name */ - readonly name: string; - /** - * Perform the filter of the data based in concrete criteria - * @param process - Data processing task object - */ - do: ( - process: JobObject - ) => JobObject; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { AnyHeaders } from './Headers.i'; +import { JobObject } from './JobObject.i'; +import { AnyOptions } from './Options.i'; + +/** Base class for strategies */ +export interface Strategy< + Type extends string = string, + Data = any, + CustomHeaders extends Record = AnyHeaders, + CustomOptions extends Record = AnyOptions, +> { + /** Strategy name */ + readonly name: string; + /** + * Perform the filter of the data based in concrete criteria + * @param process - Data processing task object + */ + do: ( + process: JobObject + ) => JobObject; +} diff --git a/packages/api/core/src/Layer/Provider/Factory.ts b/packages/api/core/src/Layer/Provider/Factory.ts index 80f589f7..64bca700 100644 --- a/packages/api/core/src/Layer/Provider/Factory.ts +++ b/packages/api/core/src/Layer/Provider/Factory.ts @@ -1,57 +1,58 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { LoggerInstance } from '@mdf.js/logger'; -import { Manager } from './Manager'; -import { Port } from './Port'; -import { Factory, FactoryOptions, PortConfigValidationStruct } from './types'; - -/** - * Create a new Provider Factory based in a Port - * @param port - Port instance - * @param validation - Port config validation struct - * @param defaultName - Default name for the provider - * @param type - Provider type - * @returns Factory class, with a static `create` methods to create a provider instances - */ -export default function >( - port: new (config: PortConfig, logger: LoggerInstance) => PortInstance, - validation: PortConfigValidationStruct, - defaultName: string, - type: string -): Factory { - return class MixinFactory { - /** Provider */ - private readonly provider: Manager; - /** - * Create a new provider - * @param options - Provider configuration options - */ - public static create( - options?: FactoryOptions - ): Manager { - return new MixinFactory(options).provider; - } - /** - * Private constructor for provider factory - * @param options - Provider configuration options - */ - private constructor(options?: FactoryOptions) { - this.provider = new Manager( - port, - { - name: options?.name || defaultName, - type, - validation, - useEnvironment: options?.useEnvironment ?? true, - logger: options?.logger, - }, - options?.config - ); - } - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { LoggerInstance } from '@mdf.js/logger'; +import { Manager } from './Manager'; +import { Port } from './Port'; +import { Factory, FactoryOptions, PortConfigValidationStruct } from './types'; + +/** + * Create a new Provider Factory based in a Port + * @param port - Port instance + * @param validation - Port config validation struct + * @param defaultName - Default name for the provider + * @param type - Provider type + * @returns Factory class, with a static `create` methods to create a provider instances + */ +export default function >( + port: new (config: PortConfig, logger: LoggerInstance) => PortInstance, + validation: PortConfigValidationStruct, + defaultName: string, + type: string +): Factory { + return class MixinFactory { + /** Provider */ + private readonly provider: Manager; + /** + * Create a new provider + * @param options - Provider configuration options + */ + public static create( + options?: FactoryOptions + ): Manager { + return new MixinFactory(options).provider; + } + /** + * Private constructor for provider factory + * @param options - Provider configuration options + */ + private constructor(options?: FactoryOptions) { + this.provider = new Manager( + port, + { + name: options?.name ?? defaultName, + type, + validation, + useEnvironment: options?.useEnvironment ?? true, + logger: options?.logger, + }, + options?.config + ); + } + }; +} + diff --git a/packages/api/core/src/Layer/Provider/Manager.ts b/packages/api/core/src/Layer/Provider/Manager.ts index aab4373d..3d66ba3a 100644 --- a/packages/api/core/src/Layer/Provider/Manager.ts +++ b/packages/api/core/src/Layer/Provider/Manager.ts @@ -1,388 +1,388 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; -import { formatEnv } from '@mdf.js/utils'; -import { EventEmitter } from 'events'; -import Joi, { ValidationError } from 'joi'; -import { cloneDeep, merge } from 'lodash'; -import { v4 } from 'uuid'; -import { Health } from '../..'; -import { overallStatus } from '../../Health'; -import { Resource } from '../App'; -import { Port } from './Port'; -import { ErrorState, State, StoppedState } from './states'; -import { ProviderOptions, ProviderState, ProviderStatus } from './types'; - -export declare interface Manager< - PortClient, - PortConfig, - PortInstance extends Port, -> { - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - on(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - addListener(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - once(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - off(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - removeListener(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `error` event - */ - removeAllListeners(event?: 'error'): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - on(event: 'status', listener: (status: ProviderStatus) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - addListener(event: 'status', listener: (status: ProviderStatus) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - once(event: 'status', listener: (status: ProviderStatus) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - off(event: 'status', listener: (status: ProviderStatus) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - removeListener(event: 'status', listener: (status: ProviderStatus) => void): this; -} - -/** - * Provider Manager wraps a specific port created by the extension of the {@link Port} abstract - * class, instrumenting it with the necessary logic to manage: - * - * - The state of the provider, represented by the {@link Manager.state} property, and managed by - * the {@link Manager.start}, {@link Manager.stop} and {@link Manager.fail} methods. - * - * ![class diagram](media/Provider-States-Methods.png) - * - * - Merge and validate the configuration of the provider represented by the generic type - * _**PortConfig**_. The manager configuration object {@link ProviderOptions} has a _**validation**_ - * property that represent a structure of type {@link PortConfigValidationStruct} where default - * values, environment based and a [Joi validation object](https://joi.dev/api/?v=17.7.0) are - * defined. During the initialization process, the manager will merge all the sources of - * configuration (default, environment and specific) and validate the result against the Joi schema. - * So, the order of priority of the configuration sources is: specific, environment and default. - * If the validation fails, the manager will use the default values and emit an error that will be - * managed by the observability layer. - * - * @category Provider - * - * @param PortClient - Underlying client type, this is, the real client of the wrapped provider - * @param PortConfig - Port configuration object, could be an extended version of the client config - * @param T - Port class, this is, the class that extends the {@link Port} abstract class - * @public - */ -export class Manager> - extends EventEmitter - implements Resource -{ - /** Provider unique identifier for trace purposes */ - public readonly componentId: string; - /** Debug logger*/ - private readonly logger: LoggerInstance; - /** Error in error state */ - private _error?: Multi | Crash; - /** Provider actual state */ - private _state: State; - /** Timestamp of actual state */ - private _date: string; - /** Port instance */ - private readonly port: PortInstance; - /** Port configuration */ - public readonly config: PortConfig; - /** - * Implementation of base functionalities of a provider manager - * @param port - Port wrapper class - * @param config - Port configuration options - * @param options - Manager configuration options - */ - constructor( - port: new (portConfig: PortConfig, logger: LoggerInstance, name: string) => PortInstance, - private readonly options: ProviderOptions, - config?: Partial - ) { - super(); - this.logger = this.options.logger || new DebugLogger(this.options.name); - this.config = this.validateConfig(config); - try { - this.port = new port(this.config, this.logger, this.options.name); - } catch (error) { - // Stryker disable next-line all - this.logger.warn(`Error trying to create an instance of the port`, v4(), this.options.name); - this.manageError(error); - throw this._error; - } - this.componentId = this.port.uuid; - this.logger = SetContext(this.logger, this.options.name, this.componentId); - this._date = new Date().toISOString(); - this.port.on('error', error => { - this.logger.error(`New error event from port: ${error.message}`, this.componentId); - this.manageError(error); - }); - if (this._error) { - this._state = this.changeState(new ErrorState(this.port, this.changeState, this.manageError)); - } else { - this._state = this.changeState( - new StoppedState(this.port, this.changeState, this.manageError) - ); - } - } - /** Return the errors in the provider */ - public get error(): Multi | Crash | undefined { - return this._error; - } - /** Provider state */ - public get state(): ProviderState { - return this._state.state; - } - /** - * Return the status of the connection in a standard format - * @returns _check object_ as defined in the draft standard - * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 - */ - public get checks(): Health.Checks { - const checks: Health.Checks = {}; - for (const [measure, check] of Object.entries(this.port.checks)) { - checks[`${this.options.name}:${measure}`] = check; - } - checks[`${this.options.name}:status`] = [ - { - status: ProviderStatus[this.state], - componentId: this.componentId, - componentType: this.options.type, - observedValue: this.state, - time: this.date, - output: this.detailedOutput(), - }, - ]; - return checks; - } - /** Provider status */ - public get status(): Health.Status { - return overallStatus(this.checks); - } - /** Port client */ - public get client(): PortClient { - return this.port.client; - } - /** Provider name */ - public get name(): string { - return this.options.name; - } - /** Timestamp of actual state in ISO format, when the current state was reached */ - public get date(): string { - return this._date; - } - /** Initialize the process: internal jobs, external dependencies connections ... */ - public async start(): Promise { - return this._state.start(); - } - /** Stop the process: internal jobs, external dependencies connections ... */ - public async stop(): Promise { - return this._state.stop(); - } - /** Close the provider: release resources, connections ... */ - public async close(): Promise { - return this.port.close(); - } - /** - * Error state: wait for new state of to fix the actual degraded stated - * @param error - Cause ot this fail transition - * @returns - */ - public async fail(error: Crash | Error): Promise { - return this._state.fail(error); - } - /** - * Change the provider state - * @param newState - state to which it transitions - */ - private readonly changeState = (newState: State): State => { - // Stryker disable next-line all - this.logger.debug(`Changing state to ${newState.state}`); - this._state = newState; - this._date = new Date().toISOString(); - if (this.listenerCount('status') > 0) { - // Stryker disable next-line all - this.logger.debug(`Emitting state change event to ${this.listenerCount('status')} listeners`); - this.emit('status', ProviderStatus[this.state]); - } - return newState; - }; - /** - * Format the error to a manageable error format - * @param error - error to be formatted - * @returns - */ - private formatError(error: unknown): Multi | Crash { - let formattedError: Multi | Crash | undefined; - if (error instanceof ValidationError) { - if ( - this._error && - this._error instanceof Multi && - this._error.findCauseByName('ValidationError') - ) { - formattedError = this._error; - } else { - formattedError = new Multi(`Error in the provider configuration process`, this.componentId); - } - formattedError.Multify(error); - } else if (error instanceof Crash || error instanceof Multi) { - formattedError = error; - } else if (error instanceof Error) { - formattedError = new Crash(error.message, this.componentId); - } else if (typeof error === 'string') { - formattedError = new Crash(error, this.componentId); - } else if ( - error && - typeof error === 'object' && - typeof (error as Record)['message'] === 'string' - ) { - formattedError = new Crash((error as Record)['message']); - } else { - formattedError = new Crash(`Unknown error in port ${this.options.name}`, this.componentId); - } - return formattedError; - } - /** - * Manage the errors in the provider (logging, emitting, last error ...) - * @param error - Error from wrapper instance - */ - private readonly manageError = (error: unknown): void => { - this._error = this.formatError(error); - // Stryker disable all - this.logger.error( - `New error event from provider: ${this._error.message}`, - this.componentId, - this.options.name - ); - this.logger.crash(this._error, this.options.name); - // Stryker enable all - if (this.listenerCount('error') > 0) { - // Stryker disable all - this.logger.debug( - `Emitting error event to ${this.listenerCount('error')} listeners`, - this.componentId, - this.options.name - ); - // Stryker enable all - this.emit('error', this._error); - } - }; - /** - * Manage the actual stored error (last error), to create a human readable output used in the - * observability (SubcomponentDetail) - */ - private detailedOutput(): string | string[] | undefined { - if (this.state === 'error' && this._error) { - return this._error.trace(); - } else { - return undefined; - } - } - /** - * Merge the configuration with the default values and environment values and perform the - * validation of the new configuration against the schema - * @param options - validation configuration options - */ - private validateConfig(config?: Partial): PortConfig { - let baseConfig: PortConfig; - const actualConfig = this.mergeConfigSources(config); - try { - baseConfig = Joi.attempt(actualConfig, this.options.validation.schema); - // Stryker disable next-line all - this.logger.info(`Configuration has been validated properly`); - } catch (error) { - // Stryker disable next-line all - this.logger.warn(`Incorrect configuration, default configuration will be used`); - this.manageError(error); - try { - baseConfig = Joi.attempt( - this.options.validation.defaultConfig, - this.options.validation.schema - ); - } catch (defaultError) { - // Stryker disable next-line all - this.logger.warn(`Default configuration is not valid too, nevertheless will be used ...`); - this.manageError(defaultError); - baseConfig = this.options.validation.defaultConfig; - } - } - return baseConfig; - } - /** - * Merge the environment configuration and the default configuration with the specific - * configuration - * @param config - specific configuration for the provider instances - * @returns - */ - private mergeConfigSources(config?: Partial): Partial { - const defaultConfig = cloneDeep(this.options.validation.defaultConfig); - let envConfig: Partial; - if (typeof this.options.useEnvironment === 'boolean' && this.options.useEnvironment) { - envConfig = this.options.validation.envBasedConfig; - } else if (typeof this.options.useEnvironment === 'string') { - envConfig = formatEnv(this.options.useEnvironment) as Partial; - } else { - envConfig = {}; - } - return merge(defaultConfig, envConfig, config); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; +import { formatEnv } from '@mdf.js/utils'; +import { EventEmitter } from 'events'; +import Joi, { ValidationError } from 'joi'; +import { cloneDeep, merge } from 'lodash'; +import { v4 } from 'uuid'; +import { Health } from '../..'; +import { overallStatus } from '../../Health'; +import { Resource } from '../App'; +import { Port } from './Port'; +import { ErrorState, State, StoppedState } from './states'; +import { ProviderOptions, ProviderState, ProviderStatus } from './types'; + +export declare interface Manager< + PortClient, + PortConfig, + PortInstance extends Port, +> { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + addListener(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + once(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + off(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + removeListener(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `error` event + */ + removeAllListeners(event?: 'error'): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: ProviderStatus) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + addListener(event: 'status', listener: (status: ProviderStatus) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + once(event: 'status', listener: (status: ProviderStatus) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + off(event: 'status', listener: (status: ProviderStatus) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + removeListener(event: 'status', listener: (status: ProviderStatus) => void): this; +} + +/** + * Provider Manager wraps a specific port created by the extension of the {@link Port} abstract + * class, instrumenting it with the necessary logic to manage: + * + * - The state of the provider, represented by the {@link Manager.state} property, and managed by + * the {@link Manager.start}, {@link Manager.stop} and {@link Manager.fail} methods. + * + * ![class diagram](../../../media/Provider-States-Methods.png) + * + * - Merge and validate the configuration of the provider represented by the generic type + * _**PortConfig**_. The manager configuration object {@link ProviderOptions} has a _**validation**_ + * property that represent a structure of type {@link PortConfigValidationStruct} where default + * values, environment based and a [Joi validation object](https://joi.dev/api/?v=17.7.0) are + * defined. During the initialization process, the manager will merge all the sources of + * configuration (default, environment and specific) and validate the result against the Joi schema. + * So, the order of priority of the configuration sources is: specific, environment and default. + * If the validation fails, the manager will use the default values and emit an error that will be + * managed by the observability layer. + * + * @category Provider + * + * @param PortClient - Underlying client type, this is, the real client of the wrapped provider + * @param PortConfig - Port configuration object, could be an extended version of the client config + * @param T - Port class, this is, the class that extends the {@link Port} abstract class + * @public + */ +export class Manager> + extends EventEmitter + implements Resource +{ + /** Provider unique identifier for trace purposes */ + public readonly componentId: string; + /** Debug logger*/ + private readonly logger: LoggerInstance; + /** Error in error state */ + private _error?: Multi | Crash; + /** Provider actual state */ + private _state: State; + /** Timestamp of actual state */ + private _date: string; + /** Port instance */ + private readonly port: PortInstance; + /** Port configuration */ + public readonly config: PortConfig; + /** + * Implementation of base functionalities of a provider manager + * @param port - Port wrapper class + * @param config - Port configuration options + * @param options - Manager configuration options + */ + constructor( + port: new (portConfig: PortConfig, logger: LoggerInstance, name: string) => PortInstance, + private readonly options: ProviderOptions, + config?: Partial + ) { + super(); + this.logger = this.options.logger || new DebugLogger(this.options.name); + this.config = this.validateConfig(config); + try { + this.port = new port(this.config, this.logger, this.options.name); + } catch (error) { + // Stryker disable next-line all + this.logger.warn(`Error trying to create an instance of the port`, v4(), this.options.name); + this.manageError(error); + throw this._error; + } + this.componentId = this.port.uuid; + this.logger = SetContext(this.logger, this.options.name, this.componentId); + this._date = new Date().toISOString(); + this.port.on('error', error => { + this.logger.error(`New error event from port: ${error.message}`, this.componentId); + this.manageError(error); + }); + if (this._error) { + this._state = this.changeState(new ErrorState(this.port, this.changeState, this.manageError)); + } else { + this._state = this.changeState( + new StoppedState(this.port, this.changeState, this.manageError) + ); + } + } + /** Return the errors in the provider */ + public get error(): Multi | Crash | undefined { + return this._error; + } + /** Provider state */ + public get state(): ProviderState { + return this._state.state; + } + /** + * Return the status of the connection in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + const checks: Health.Checks = {}; + for (const [measure, check] of Object.entries(this.port.checks)) { + checks[`${this.options.name}:${measure}`] = check; + } + checks[`${this.options.name}:status`] = [ + { + status: ProviderStatus[this.state], + componentId: this.componentId, + componentType: this.options.type, + observedValue: this.state, + time: this.date, + output: this.detailedOutput(), + }, + ]; + return checks; + } + /** Provider status */ + public get status(): Health.Status { + return overallStatus(this.checks); + } + /** Port client */ + public get client(): PortClient { + return this.port.client; + } + /** Provider name */ + public get name(): string { + return this.options.name; + } + /** Timestamp of actual state in ISO format, when the current state was reached */ + public get date(): string { + return this._date; + } + /** Initialize the process: internal jobs, external dependencies connections ... */ + public async start(): Promise { + return this._state.start(); + } + /** Stop the process: internal jobs, external dependencies connections ... */ + public async stop(): Promise { + return this._state.stop(); + } + /** Close the provider: release resources, connections ... */ + public async close(): Promise { + return this.port.close(); + } + /** + * Error state: wait for new state of to fix the actual degraded stated + * @param error - Cause ot this fail transition + * @returns + */ + public async fail(error: Crash | Error): Promise { + return this._state.fail(error); + } + /** + * Change the provider state + * @param newState - state to which it transitions + */ + private readonly changeState = (newState: State): State => { + // Stryker disable next-line all + this.logger.debug(`Changing state to ${newState.state}`); + this._state = newState; + this._date = new Date().toISOString(); + if (this.listenerCount('status') > 0) { + // Stryker disable next-line all + this.logger.debug(`Emitting state change event to ${this.listenerCount('status')} listeners`); + this.emit('status', ProviderStatus[this.state]); + } + return newState; + }; + /** + * Format the error to a manageable error format + * @param error - error to be formatted + * @returns + */ + private formatError(error: unknown): Multi | Crash { + let formattedError: Multi | Crash | undefined; + if (error instanceof ValidationError) { + if ( + this._error && + this._error instanceof Multi && + this._error.findCauseByName('ValidationError') + ) { + formattedError = this._error; + } else { + formattedError = new Multi(`Error in the provider configuration process`, this.componentId); + } + formattedError.Multify(error); + } else if (error instanceof Crash || error instanceof Multi) { + formattedError = error; + } else if (error instanceof Error) { + formattedError = new Crash(error.message, this.componentId); + } else if (typeof error === 'string') { + formattedError = new Crash(error, this.componentId); + } else if ( + error && + typeof error === 'object' && + typeof (error as Record)['message'] === 'string' + ) { + formattedError = new Crash((error as Record)['message']); + } else { + formattedError = new Crash(`Unknown error in port ${this.options.name}`, this.componentId); + } + return formattedError; + } + /** + * Manage the errors in the provider (logging, emitting, last error ...) + * @param error - Error from wrapper instance + */ + private readonly manageError = (error: unknown): void => { + this._error = this.formatError(error); + // Stryker disable all + this.logger.error( + `New error event from provider: ${this._error.message}`, + this.componentId, + this.options.name + ); + this.logger.crash(this._error, this.options.name); + // Stryker enable all + if (this.listenerCount('error') > 0) { + // Stryker disable all + this.logger.debug( + `Emitting error event to ${this.listenerCount('error')} listeners`, + this.componentId, + this.options.name + ); + // Stryker enable all + this.emit('error', this._error); + } + }; + /** + * Manage the actual stored error (last error), to create a human readable output used in the + * observability (SubcomponentDetail) + */ + private detailedOutput(): string | string[] | undefined { + if (this.state === 'error' && this._error) { + return this._error.trace(); + } else { + return undefined; + } + } + /** + * Merge the configuration with the default values and environment values and perform the + * validation of the new configuration against the schema + * @param options - validation configuration options + */ + private validateConfig(config?: Partial): PortConfig { + let baseConfig: PortConfig; + const actualConfig = this.mergeConfigSources(config); + try { + baseConfig = Joi.attempt(actualConfig, this.options.validation.schema); + // Stryker disable next-line all + this.logger.info(`Configuration has been validated properly`); + } catch (error) { + // Stryker disable next-line all + this.logger.warn(`Incorrect configuration, default configuration will be used`); + this.manageError(error); + try { + baseConfig = Joi.attempt( + this.options.validation.defaultConfig, + this.options.validation.schema + ); + } catch (defaultError) { + // Stryker disable next-line all + this.logger.warn(`Default configuration is not valid too, nevertheless will be used ...`); + this.manageError(defaultError); + baseConfig = this.options.validation.defaultConfig; + } + } + return baseConfig; + } + /** + * Merge the environment configuration and the default configuration with the specific + * configuration + * @param config - specific configuration for the provider instances + * @returns + */ + private mergeConfigSources(config?: Partial): Partial { + const defaultConfig = cloneDeep(this.options.validation.defaultConfig); + let envConfig: Partial; + if (typeof this.options.useEnvironment === 'boolean' && this.options.useEnvironment) { + envConfig = this.options.validation.envBasedConfig; + } else if (typeof this.options.useEnvironment === 'string') { + envConfig = formatEnv(this.options.useEnvironment); + } else { + envConfig = {}; + } + return merge(defaultConfig, envConfig, config); + } +} diff --git a/packages/api/core/src/Layer/Provider/Port.ts b/packages/api/core/src/Layer/Provider/Port.ts index cba08b79..45e07b3b 100644 --- a/packages/api/core/src/Layer/Provider/Port.ts +++ b/packages/api/core/src/Layer/Provider/Port.ts @@ -1,235 +1,235 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { LoggerInstance, SetContext } from '@mdf.js/logger'; -import { EventEmitter } from 'events'; -import { v4 } from 'uuid'; -import { Health } from '../..'; - -export declare interface Port { - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - on(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - once(event: 'error', listener: (error: Crash | Multi) => void): this; - /** - * Emit an `error` event, to notify errors in the resource management or access, this will change - * the provider state by the upper manager. - * @param event - `error` event - * @param error - Error to be notified to the upper manager - * @event - */ - emit(event: 'error', error: Crash | Multi): boolean; - /** - * Add a listener for the `closed` event, emitted when the port resources are no longer available - * @param event - `closed` event - * @param listener - Closed event listener - * @event - */ - on(event: 'closed', listener: (error?: Crash | Multi) => void): this; - /** - * Add a listener for the `closed` event, emitted when the port resources are no longer available. - * This is a one-time event, the listener will be removed after the first emission. - * @param event - `closed` event - * @param listener - Closed event listener - * @event - */ - once(event: 'closed', listener: (error?: Crash | Multi) => void): this; - /** - * Emit a `closed` event, to notify that the access to the resources is not longer possible. This - * event should not be emitted if {@link Port.stop} or {@link Port.close } methods are used. This - * event will change the provider state by the upper manager. - * @param event - `closed` event - * @param error - Error to be notified to the upper manager, if any - * @event - */ - emit(event: 'closed', error?: Crash | Multi): boolean; - /** - * Add a listener for the `unhealthy` event, emitted when the port has limited access to the - * resources - * @param event - `unhealthy` event - * @param listener - Unhealthy event listener - * @event - */ - on(event: 'unhealthy', listener: (error: Crash | Multi) => void): this; - /** - * Add a listener for the `unhealthy` event, emitted when the port has limited access to the - * resources. This is a one-time event, the listener will be removed after the first emission. - * @param event - `unhealthy` event - * @param listener - Unhealthy event listener - * @event - */ - once(event: 'unhealthy', listener: (error: Crash | Multi) => void): this; - /** - * Emit an `unhealthy` event, to notify that the port has limited access to the resources. This - * event will change the provider state by the upper manager. - * @param event - `unhealthy` event - * @param error - Error to be notified to the upper manager - * @event - */ - emit(event: 'unhealthy', error: Crash | Multi): boolean; - /** - * Add a listener for the `healthy` event, emitted when the port has recovered the access to the - * resources - * @param event - `healthy` event - * @param listener - Healthy event listener - * @event - */ - on(event: 'healthy', listener: () => void): this; - /** - * Add a listener for the `healthy` event, emitted when the port has recovered the access to the - * resources. This is a one-time event, the listener will be removed after the first emission. - * @param event - `healthy` event - * @param listener - Healthy event listener - * @event - */ - once(event: 'healthy', listener: () => void): this; - /** - * Emit a `healthy` event, to notify that the port has recovered the access to the resources. This - * event will change the provider state by the upper manager. - * @param event - `healthy` event - * @event - */ - emit(event: 'healthy'): boolean; -} -/** - * This is the class that should be extended to implement a new specific Port. - * - * This class implements some util logic to facilitate the creation of new Ports, for this reason is - * exposed as abstract class, instead of an interface. The basic operations that already implemented - * in the class are: - * - * - {@link Health.Checks } management: using the {@link Port.addCheck} method is - * possible to include new observed values that will be used in the observability layers. - * - Create a {@link Port.uuid} unique identifier for the port instance, this uuid is used in error - * traceability. - * - Establish the context for the logger to simplify the identification of the port in the logs, - * this is, it's not necessary to indicate the uuid and context in each logging function call. - * - Store the configuration _**PortConfig**_ previously validated by the Manager. - * - * What the user of this class should develop in the specific port: - * - * - The {@link Port.start} method, which is responsible initialize or stablish the connection to - * the resources. - * - The {@link Port.stop} method, which is responsible stop services or disconnect from the - * resources. - * - The {@link Port.close} method, which is responsible to destroy the services, resources or - * perform a simple disconnection. - * - The {@link Port.state} property, a boolean value that indicates if the port is connected - * healthy (true) or not (false). - * - The {@link Port.client} property, that return the _**PortClient**_ instance that is used to - * interact with the resources. - * - * ![class diagram](media/Provider-Class-Hierarchy.png) - * - * In the other hand, this class extends the {@link EventEmitter} class, so it's possible to emit - * events to notify the status of the port: - * - * - _**error**_: should be emitted to notify errors in the resource management or access, this will - * not change the provider state, but the error will be registered in the observability layers. - * - _**closed**_: should be emitted if the access to the resources is not longer possible. This - * event should not be emitted if {@link Port.stop} or {@link Port.close } methods are used. - * - _**unhealthy**_: should be emitted when the port has limited access to the resources. - * - _**healthy**_: should be emitted when the port has recovered the access to the resources. - * - * ![class diagram](media/Provider-States-Events.png) - * - * Check some examples of implementation in: - * - * - [Elastic provider](https://www.npmjs.com/package/@mdf.js/elastic-provider) - * - [Mongo Provider](https://www.npmjs.com/package/@mdf.js/mongo-provider) - * @category Provider - * @param PortClient - Underlying client type, this is, the real client of the wrapped provider - * @param PortConfig - Port configuration object, could be an extended version of the client config - * @public - */ -export abstract class Port extends EventEmitter { - /** Port unique identifier for trace purposes */ - public readonly uuid: string = v4(); - /** Port logger, to be used internally */ - protected readonly logger: LoggerInstance; - /** Port diagnostic checks */ - private readonly checksMap: Map = new Map(); - /** - * Abstract implementation of basic functionalities of a Port - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - * @param name - Port name, to be used as identifier - */ - constructor( - public readonly config: PortConfig, - logger: LoggerInstance, - public readonly name: string - ) { - super(); - this.logger = SetContext(logger, this.name, this.uuid); - } - /** - * Update or add a check measure. - * This should be used to inform about the state of resources behind the Port, for example memory - * usage, CPU usage, etc. - * - * The new check will be taking into account in the overall health status. - * The new check will be included in the `checks` object with the key indicated in the param - * `measure`.* If this key already exists, the `componentId` of the `check` parameter will be - * checked, if there is a check with the same `componentId` in the array, the check will be - * updated, in other case the new check will be added to the existing array. - * - * The maximum number external checks is 100 - * @param measure - measure identification - * @param check - check to be updated or included - * @returns true, if the check has been updated - * @public - */ - protected addCheck(measure: string, check: Health.Check): boolean { - if ( - (check.status && !Health.STATUSES.includes(check.status)) || - typeof check.componentId !== 'string' || - this.checksMap.size >= 100 - ) { - return false; - } - const checks = this.checksMap.get(measure) || []; - const entryIndex = checks.findIndex(entry => entry.componentId === check.componentId); - if (entryIndex === -1) { - checks.push(check); - } else { - checks[entryIndex] = check; - } - this.checksMap.set(measure, checks); - return true; - } - /** Return the actual checks */ - public get checks(): Record { - const checks: Record = {}; - for (const [measure, checksArray] of this.checksMap) { - checks[measure] = checksArray; - } - return checks; - } - /** Return the underlying port client */ - public abstract get client(): PortClient; - /** Return the port state as a boolean value, true if the port is available, false in otherwise */ - public abstract get state(): boolean; - /** Start the port, making it available */ - public abstract start(): Promise; - /** Stop the port, making it unavailable */ - public abstract stop(): Promise; - /** Close the port, making it no longer available */ - public abstract close(): Promise; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { LoggerInstance, SetContext } from '@mdf.js/logger'; +import { EventEmitter } from 'events'; +import { v4 } from 'uuid'; +import { Health } from '../..'; + +export declare interface Port { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + once(event: 'error', listener: (error: Crash | Multi) => void): this; + /** + * Emit an `error` event, to notify errors in the resource management or access, this will change + * the provider state by the upper manager. + * @param event - `error` event + * @param error - Error to be notified to the upper manager + * @event + */ + emit(event: 'error', error: Crash | Multi): boolean; + /** + * Add a listener for the `closed` event, emitted when the port resources are no longer available + * @param event - `closed` event + * @param listener - Closed event listener + * @event + */ + on(event: 'closed', listener: (error?: Crash | Multi) => void): this; + /** + * Add a listener for the `closed` event, emitted when the port resources are no longer available. + * This is a one-time event, the listener will be removed after the first emission. + * @param event - `closed` event + * @param listener - Closed event listener + * @event + */ + once(event: 'closed', listener: (error?: Crash | Multi) => void): this; + /** + * Emit a `closed` event, to notify that the access to the resources is not longer possible. This + * event should not be emitted if {@link Port.stop} or {@link Port.close } methods are used. This + * event will change the provider state by the upper manager. + * @param event - `closed` event + * @param error - Error to be notified to the upper manager, if any + * @event + */ + emit(event: 'closed', error?: Crash | Multi): boolean; + /** + * Add a listener for the `unhealthy` event, emitted when the port has limited access to the + * resources + * @param event - `unhealthy` event + * @param listener - Unhealthy event listener + * @event + */ + on(event: 'unhealthy', listener: (error: Crash | Multi) => void): this; + /** + * Add a listener for the `unhealthy` event, emitted when the port has limited access to the + * resources. This is a one-time event, the listener will be removed after the first emission. + * @param event - `unhealthy` event + * @param listener - Unhealthy event listener + * @event + */ + once(event: 'unhealthy', listener: (error: Crash | Multi) => void): this; + /** + * Emit an `unhealthy` event, to notify that the port has limited access to the resources. This + * event will change the provider state by the upper manager. + * @param event - `unhealthy` event + * @param error - Error to be notified to the upper manager + * @event + */ + emit(event: 'unhealthy', error: Crash | Multi): boolean; + /** + * Add a listener for the `healthy` event, emitted when the port has recovered the access to the + * resources + * @param event - `healthy` event + * @param listener - Healthy event listener + * @event + */ + on(event: 'healthy', listener: () => void): this; + /** + * Add a listener for the `healthy` event, emitted when the port has recovered the access to the + * resources. This is a one-time event, the listener will be removed after the first emission. + * @param event - `healthy` event + * @param listener - Healthy event listener + * @event + */ + once(event: 'healthy', listener: () => void): this; + /** + * Emit a `healthy` event, to notify that the port has recovered the access to the resources. This + * event will change the provider state by the upper manager. + * @param event - `healthy` event + * @event + */ + emit(event: 'healthy'): boolean; +} +/** + * This is the class that should be extended to implement a new specific Port. + * + * This class implements some util logic to facilitate the creation of new Ports, for this reason is + * exposed as abstract class, instead of an interface. The basic operations that already implemented + * in the class are: + * + * - {@link Health.Checks } management: using the {@link Port.addCheck} method is + * possible to include new observed values that will be used in the observability layers. + * - Create a {@link Port.uuid} unique identifier for the port instance, this uuid is used in error + * traceability. + * - Establish the context for the logger to simplify the identification of the port in the logs, + * this is, it's not necessary to indicate the uuid and context in each logging function call. + * - Store the configuration _**PortConfig**_ previously validated by the Manager. + * + * What the user of this class should develop in the specific port: + * + * - The {@link Port.start} method, which is responsible initialize or stablish the connection to + * the resources. + * - The {@link Port.stop} method, which is responsible stop services or disconnect from the + * resources. + * - The {@link Port.close} method, which is responsible to destroy the services, resources or + * perform a simple disconnection. + * - The {@link Port.state} property, a boolean value that indicates if the port is connected + * healthy (true) or not (false). + * - The {@link Port.client} property, that return the _**PortClient**_ instance that is used to + * interact with the resources. + * + * ![class diagram](../../../media/Provider-Class-Hierarchy.png) + * + * In the other hand, this class extends the `EventEmitter` class, so it's possible to emit + * events to notify the status of the port: + * + * - _**error**_: should be emitted to notify errors in the resource management or access, this will + * not change the provider state, but the error will be registered in the observability layers. + * - _**closed**_: should be emitted if the access to the resources is not longer possible. This + * event should not be emitted if {@link Port.stop} or {@link Port.close } methods are used. + * - _**unhealthy**_: should be emitted when the port has limited access to the resources. + * - _**healthy**_: should be emitted when the port has recovered the access to the resources. + * + * ![class diagram](../../../media/Provider-States-Events.png) + * + * Check some examples of implementation in: + * + * - [Elastic provider](https://www.npmjs.com/package/@mdf.js/elastic-provider) + * - [Mongo Provider](https://www.npmjs.com/package/@mdf.js/mongo-provider) + * @category Provider + * @param PortClient - Underlying client type, this is, the real client of the wrapped provider + * @param PortConfig - Port configuration object, could be an extended version of the client config + * @public + */ +export abstract class Port extends EventEmitter { + /** Port unique identifier for trace purposes */ + public readonly uuid: string = v4(); + /** Port logger, to be used internally */ + protected readonly logger: LoggerInstance; + /** Port diagnostic checks */ + private readonly checksMap: Map = new Map(); + /** + * Abstract implementation of basic functionalities of a Port + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + * @param name - Port name, to be used as identifier + */ + constructor( + public readonly config: PortConfig, + logger: LoggerInstance, + public readonly name: string + ) { + super(); + this.logger = SetContext(logger, this.name, this.uuid); + } + /** + * Update or add a check measure. + * This should be used to inform about the state of resources behind the Port, for example memory + * usage, CPU usage, etc. + * + * The new check will be taking into account in the overall health status. + * The new check will be included in the `checks` object with the key indicated in the param + * `measure`.* If this key already exists, the `componentId` of the `check` parameter will be + * checked, if there is a check with the same `componentId` in the array, the check will be + * updated, in other case the new check will be added to the existing array. + * + * The maximum number external checks is 100 + * @param measure - measure identification + * @param check - check to be updated or included + * @returns true, if the check has been updated + * @public + */ + protected addCheck(measure: string, check: Health.Check): boolean { + if ( + (check.status && !Health.STATUSES.includes(check.status)) || + typeof check.componentId !== 'string' || + this.checksMap.size >= 100 + ) { + return false; + } + const checks = this.checksMap.get(measure) || []; + const entryIndex = checks.findIndex(entry => entry.componentId === check.componentId); + if (entryIndex === -1) { + checks.push(check); + } else { + checks[entryIndex] = check; + } + this.checksMap.set(measure, checks); + return true; + } + /** Return the actual checks */ + public get checks(): Record { + const checks: Record = {}; + for (const [measure, checksArray] of this.checksMap) { + checks[measure] = checksArray; + } + return checks; + } + /** Return the underlying port client */ + public abstract get client(): PortClient; + /** Return the port state as a boolean value, true if the port is available, false in otherwise */ + public abstract get state(): boolean; + /** Start the port, making it available */ + public abstract start(): Promise; + /** Stop the port, making it unavailable */ + public abstract stop(): Promise; + /** Close the port, making it no longer available */ + public abstract close(): Promise; +} diff --git a/packages/api/crash/README.md b/packages/api/crash/README.md index df364dc5..c6918c94 100644 --- a/packages/api/crash/README.md +++ b/packages/api/crash/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/crash/package.json b/packages/api/crash/package.json index 9daf1bc2..8515d1a3 100644 --- a/packages/api/crash/package.json +++ b/packages/api/crash/package.json @@ -1,52 +1,51 @@ -{ - "name": "@mdf.js/crash", - "version": "0.0.1", - "description": "MMS - API Crash - Enhanced error management library", - "keywords": [ - "NodeJS", - "MMS", - "API", - "error", - "crash", - "multi", - "boom" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/crash" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "doc": "typedoc --options typedoc.json", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/crash/licenses.csv --customPath ../../../.config/customFormat.json", - "lint": "eslint \"src/**/*.ts\" --quiet --fix", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/uuid": "^10.0.0", - "joi": "^17.13.3" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/crash", + "version": "0.0.1", + "description": "MMS - API Crash - Enhanced error management library", + "keywords": [ + "NodeJS", + "MMS", + "API", + "error", + "crash", + "multi", + "boom" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/crash" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "doc": "typedoc --options typedoc.json", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/crash/licenses.csv --customPath ../../../.config/customFormat.json", + "lint": "eslint \"src/**/*.ts\" --quiet --fix", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "joi": "^17.13.3" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/crash/src/Boom/BoomError.ts b/packages/api/crash/src/Boom/BoomError.ts index 78ffad92..5aac641a 100644 --- a/packages/api/crash/src/Boom/BoomError.ts +++ b/packages/api/crash/src/Boom/BoomError.ts @@ -1,227 +1,226 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Cause } from '..'; -import { Base } from '../BaseError'; -import { HTTP_CODES } from '../const'; -import { Crash } from '../Crash'; -import { Multi } from '../Multi'; -import { APIError, APISource, BoomOptions, ContextLink, Links, ValidationError } from '../types'; - -/** - * Improved error handling in REST-API interfaces - * - * - * Boom helps us with error responses (HTTP Codes 3XX-5XX) within our REST-API interface by - * providing us with some tools: - * - Helpers for the rapid generation of standard responses. - * - Association of errors and their causes in a hierarchical way. - * - Adaptation of validation errors of the Joi library. - * - * In addition, in combination with the Multi error types, errors in validation processes, and - * Crash, standard application errors, it allows a complete management of the different types of - * errors in our backend. - * @category Boom - * @public - */ -export class Boom extends Base { - /** Boom error cause */ - private _cause?: Cause; - /** Boom error code */ - private readonly _code: number; - /** Links that leads to further details about this particular occurrence of the problem */ - private readonly _links?: Links; - /** An object containing references to the source of the error */ - private readonly _source?: APISource; - /** Boom error */ - private readonly _isBoom = true; - /** - * Create a new Boom error - * @param message - human friendly error message - * @param uuid - unique identifier for this particular occurrence of the problem - * @param code - HTTP Standard error code - * @param options - enhanced error options - */ - constructor(message: string, uuid: string, code = 500, options?: BoomOptions) { - super(message, uuid, { - name: options?.cause?.name || 'HTTP', - info: options?.info, - }); - // ***************************************************************************************** - // #region code type safe - if (typeof code !== 'number') { - throw new Crash('Code must be a number', uuid); - } - this._code = code; - // #endregion - // ***************************************************************************************** - // #region options type safe - this._cause = this.typeSafeCause(uuid, options?.cause); - // #endregion - if (!this.typeSafeLinks(options?.links) || !this.typeSafeSource(options?.source)) { - throw new Crash('Links and source must be strings', uuid); - } - this._links = options?.links; - this._source = options?.source; - if (this.name === 'BaseError') { - this.name = 'HTTPError'; - } - } - /** Return APIError in JSON format */ - public toJSON(): APIError { - return { - uuid: this._uuid, - links: this._links, - status: this._code, - code: this.name, - title: HTTP_CODES.get(this._code) || 'Undefined error', - detail: this.message, - source: this._source, - meta: this._options?.info, - }; - } - /** Boom error code */ - public get status(): number { - return this._code; - } - /** - * Links that leads to further details about this particular occurrence of the problem. - * A link MUST be represented as either: - * - self: a string containing the link’s URL - * - related: an object (“link object”) which can contain the following members: - * - href: a string containing the link’s URL. - * - meta: a meta object containing non-standard meta-information about the link. - */ - public get links(): Links | undefined { - return this._links; - } - /** - * Object with the key information of the requested resource in the REST API context - * @deprecated - `source` has been deprecated, use resource instead - */ - public get source(): APISource | undefined { - return this._source; - } - /** Object with the key information of the requested resource in the REST API context */ - public get resource(): APISource | undefined { - return this._source; - } - /** Boom error */ - public get isBoom(): boolean { - return this._isBoom; - } - /** Cause source of error */ - public override get cause(): Cause | undefined { - return this._cause; - } - /** Get the trace of this hierarchy of errors */ - public trace(): string[] { - const trace: string[] = []; - let cause = this._cause; - while (cause) { - if (cause instanceof Multi) { - trace.push(`caused by ${cause.toString()}`); - if (cause.causes) { - trace.push(...cause.causes.map(entry => `failed with ${entry.toString()}`)); - } - cause = undefined; - } else if (cause instanceof Crash) { - trace.push(`caused by ${cause.toString()}`); - cause = cause.cause; - } else { - trace.push(`caused by ${cause.name}: ${cause.message}`); - cause = undefined; - } - } - trace.unshift(this.toString()); - return trace; - } - /** - * Transform joi Validation error in a Boom error - * @param error - `ValidationError` from a Joi validation process - * @param uuid - unique identifier for this particular occurrence of the problem - */ - public Boomify(error: ValidationError): void { - if (error.name === 'ValidationError') { - if (error.details.length > 1) { - this._cause = new Multi(error.message, this._uuid, { name: 'ValidationError' }); - (this._cause as Multi).Multify(error); - } else { - this._cause = new Crash(error.message, this._uuid, { - name: 'ValidationError', - info: error.details[0], - }); - } - } - } - /** - * Check if the cause are type safe and valid - * @param uuid - unique identifier for this particular occurrence of the problem - * @param cause - Crash error cause - */ - private typeSafeCause(uuid: string, cause?: Error | Crash): Error | Crash | undefined { - if (cause) { - if (cause instanceof Crash || cause instanceof Error) { - return cause; - } else { - throw new Crash('Parameter cause must be an Error/Crash', uuid); - } - } else { - return undefined; - } - } - /** - * Check if links are type safe and valid - * @param links - Information links for error - */ - private typeSafeLinks(links?: Links): boolean { - if (typeof links === 'object') { - let check = true; - for (const key of Object.keys(links)) { - if (typeof links[key] === 'object') { - check = this.typeSafeContextLinks(links[key] as ContextLink); - } else if (typeof links[key] !== 'string') { - check = false; - } - } - return check; - } else if (links === undefined) { - return true; - } else { - return false; - } - } - /** - * Check if links are type safe and valid - * @param links - Information links for error - */ - private typeSafeContextLinks(links?: ContextLink): boolean { - if (typeof links === 'object') { - let check = true; - for (const key of Object.keys(links)) { - if (typeof links[key] !== 'string') { - check = false; - } - } - return check; - } else if (links === undefined) { - return true; - } else { - return false; - } - } - /** - * Check if source are type safe and valid - * @param source - Source of error - */ - private typeSafeSource(source?: APISource): boolean { - if (source !== undefined) { - return typeof source.pointer === 'string'; - } else { - return true; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Cause } from '..'; +import { Base } from '../BaseError'; +import { HTTP_CODES } from '../const'; +import { Crash } from '../Crash'; +import { Multi } from '../Multi'; +import { APIError, APISource, BoomOptions, ContextLink, Links, ValidationError } from '../types'; + +/** + * Improved error handling in REST-API interfaces + * + * + * Boom helps us with error responses (HTTP Codes 3XX-5XX) within our REST-API interface by + * providing us with some tools: + * - Helpers for the rapid generation of standard responses. + * - Association of errors and their causes in a hierarchical way. + * - Adaptation of validation errors of the Joi library. + * + * In addition, in combination with the Multi error types, errors in validation processes, and + * Crash, standard application errors, it allows a complete management of the different types of + * errors in our backend. + * @category Boom + * @public + */ +export class Boom extends Base { + /** Boom error cause */ + private _cause?: Cause; + /** Boom error code */ + private readonly _code: number; + /** Links that leads to further details about this particular occurrence of the problem */ + private readonly _links?: Links; + /** An object containing references to the source of the error */ + private readonly _source?: APISource; + /** Boom error */ + private readonly _isBoom = true; + /** + * Create a new Boom error + * @param message - human friendly error message + * @param uuid - unique identifier for this particular occurrence of the problem + * @param code - HTTP Standard error code + * @param options - enhanced error options + */ + constructor(message: string, uuid: string, code = 500, options?: BoomOptions) { + super(message, uuid, { + name: options?.cause?.name ?? 'HTTP', + info: options?.info, + }); + // ***************************************************************************************** + // #region code type safe + if (typeof code !== 'number') { + throw new Crash('Code must be a number', uuid); + } + this._code = code; + // #endregion + // ***************************************************************************************** + // #region options type safe + this._cause = this.typeSafeCause(uuid, options?.cause); + // #endregion + if (!this.typeSafeLinks(options?.links) || !this.typeSafeSource(options?.source)) { + throw new Crash('Links and source must be strings', uuid); + } + this._links = options?.links; + this._source = options?.source; + if (this.name === 'BaseError') { + this.name = 'HTTPError'; + } + } + /** Return APIError in JSON format */ + public toJSON(): APIError { + return { + uuid: this._uuid, + links: this._links, + status: this._code, + code: this.name, + title: HTTP_CODES.get(this._code) ?? 'Undefined error', + detail: this.message, + source: this._source, + meta: this._options?.info, + }; + } + /** Boom error code */ + public get status(): number { + return this._code; + } + /** + * Links that leads to further details about this particular occurrence of the problem. + * A link MUST be represented as either: + * - self: a string containing the link’s URL + * - related: an object (“link object”) which can contain the following members: + * - href: a string containing the link’s URL. + * - meta: a meta object containing non-standard meta-information about the link. + */ + public get links(): Links | undefined { + return this._links; + } + /** + * Object with the key information of the requested resource in the REST API context + * @deprecated - `source` has been deprecated, use resource instead + */ + public get source(): APISource | undefined { + return this._source; + } + /** Object with the key information of the requested resource in the REST API context */ + public get resource(): APISource | undefined { + return this._source; + } + /** Boom error */ + public get isBoom(): boolean { + return this._isBoom; + } + /** Cause source of error */ + public override get cause(): Cause | undefined { + return this._cause; + } + /** Get the trace of this hierarchy of errors */ + public trace(): string[] { + const trace: string[] = []; + let cause = this._cause; + while (cause) { + if (cause instanceof Multi) { + trace.push(`caused by ${cause.toString()}`); + if (cause.causes) { + trace.push(...cause.causes.map(entry => `failed with ${entry.toString()}`)); + } + cause = undefined; + } else if (cause instanceof Crash) { + trace.push(`caused by ${cause.toString()}`); + cause = cause.cause; + } else { + trace.push(`caused by ${cause.name}: ${cause.message}`); + cause = undefined; + } + } + trace.unshift(this.toString()); + return trace; + } + /** + * Transform joi Validation error in a Boom error + * @param error - `ValidationError` from a Joi validation process + */ + public Boomify(error: ValidationError): void { + if (error.name === 'ValidationError') { + if (error.details.length > 1) { + this._cause = new Multi(error.message, this._uuid, { name: 'ValidationError' }); + (this._cause as Multi).Multify(error); + } else { + this._cause = new Crash(error.message, this._uuid, { + name: 'ValidationError', + info: error.details[0], + }); + } + } + } + /** + * Check if the cause are type safe and valid + * @param uuid - unique identifier for this particular occurrence of the problem + * @param cause - Crash error cause + */ + private typeSafeCause(uuid: string, cause?: Error | Crash): Error | Crash | undefined { + if (cause) { + if (cause instanceof Crash || cause instanceof Error) { + return cause; + } else { + throw new Crash('Parameter cause must be an Error/Crash', uuid); + } + } else { + return undefined; + } + } + /** + * Check if links are type safe and valid + * @param links - Information links for error + */ + private typeSafeLinks(links?: Links): boolean { + if (typeof links === 'object') { + let check = true; + for (const key of Object.keys(links)) { + if (typeof links[key] === 'object') { + check = this.typeSafeContextLinks(links[key]); + } else if (typeof links[key] !== 'string') { + check = false; + } + } + return check; + } else if (links === undefined) { + return true; + } else { + return false; + } + } + /** + * Check if links are type safe and valid + * @param links - Information links for error + */ + private typeSafeContextLinks(links?: ContextLink): boolean { + if (typeof links === 'object') { + let check = true; + for (const key of Object.keys(links)) { + if (typeof links[key] !== 'string') { + check = false; + } + } + return check; + } else if (links === undefined) { + return true; + } else { + return false; + } + } + /** + * Check if source are type safe and valid + * @param source - Source of error + */ + private typeSafeSource(source?: APISource): boolean { + if (source !== undefined) { + return typeof source.pointer === 'string'; + } else { + return true; + } + } +} diff --git a/packages/api/crash/src/Boom/BoomHelpers.ts b/packages/api/crash/src/Boom/BoomHelpers.ts index a9dc7934..987b81d6 100644 --- a/packages/api/crash/src/Boom/BoomHelpers.ts +++ b/packages/api/crash/src/Boom/BoomHelpers.ts @@ -1,562 +1,639 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. aaa - */ - -import { BoomOptions } from '../types'; -import { HTTPCode } from '../types/HTTPCode.t'; -import { Boom } from './BoomError'; - -/** - * Helpers for easy generation of Boom kind errors - * - Client error (`400`-`499`) - * - {@link badRequest | `400 Bad Request`} - * - {@link unauthorized | `401 Unauthorized`} - * - {@link paymentRequired | `402 Payment Required`} - * - {@link forbidden | `403 Forbidden`} - * - {@link notFound | `404 Not Found`} - * - {@link methodNotAllowed | `405 Method Not Allowed`} - * - {@link notAcceptable | `406 Not Acceptable`} - * - {@link proxyAuthRequired | `407 Proxy Authentication Required`} - * - {@link requestTimeout | `408 Request Timeout`} - * - {@link conflict | `409 Conflict`} - * - {@link gone | `410 Gone`} - * - {@link lengthRequired | `411 Length Required`} - * - {@link preconditionFailed | `412 Precondition Failed`} - * - {@link payloadTooLarge | `413 Payload Too Large`} - * - {@link uriTooLong | `414 URI Too Long`} - * - {@link unsupportedMediaType | `415 Unsupported Media Type`} - * - {@link rangeNotSatisfiable | `416 Range Not Satisfiable`} - * - {@link expectationFailed | `417 Expectation Failed`} - * - {@link teapot | `418 I'm a teapot`} - * - {@link unprocessableEntity | `422 Unprocessable Entity`} - * - {@link locked | `423 Locked`} - * - {@link failedDependency | `424 Failed Dependency`} - * - {@link tooEarly | `425 Too Early`} - * - {@link upgradeRequired | `426 Upgrade Required`} - * - {@link preconditionRequired | `428 Precondition Required`} - * - {@link tooManyRequests | `429 Too Many Requests`} - * - {@link headerFieldsTooLarge | `431 Request Header Fields Too Large`} - * - {@link illegal | `451 Unavailable For Legal Reasons`} - * - Server error (`500`-`599`) - * - {@link internalServerError | `500 Internal Server Error`} - * - {@link notImplemented | `501 Not Implemented`} - * - {@link badGateway | `502 Bad Gateway`} - * - {@link serverUnavailable | `503 Service Unavailable`} - * - {@link gatewayTimeout | `504 Gateway Timeout`} - * @category Boom - * @public - */ -export class BoomHelpers { - /** Private constructor */ - private constructor() {} - /** The HyperText Transfer Protocol (HTTP) 400 Bad Request response status code indicates that the - * server cannot or will not process the request due to something that is perceived to be a client - * error (e.g., malformed request syntax, invalid request message framing, or deceptive request - * routing). - * The client should not repeat this request without modification. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static badRequest = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.BAD_REQUEST, options); - }; - /** - * The HTTP 401 Unauthorized client error status response code indicates that the request has not - * been applied because it lacks valid authentication credentials for the target resource. - * This status is sent with a WWW-Authenticate header that contains information on how to - * authorize correctly. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static unauthorized = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.UNAUTHORIZED, options); - }; - /** - * The HTTP 402 Payment Required is a nonstandard client error status response code that is - * reserved for future use. - * Sometimes, this code indicates that the request can not be processed until the client makes a - * payment. Originally it was created to enable digital cash or (micro) payment systems and would - * indicate that the requested content is not available until the client makes a payment. However, - * no standard use convention exists and different entities use it in different contexts. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static paymentRequired = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.PAYMENT_REQUIRED, options); - }; - /** - * The HTTP 403 Forbidden client error status response code indicates that the server understood - * the request but refuses to authorize it. - * This status is similar to 401, but in this case, re-authenticating will make no difference. The - * access is permanently forbidden and tied to the application logic, such as insufficient rights - * to a resource. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static forbidden = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.FORBIDDEN, options); - }; - /** - * The HTTP 404 Not Found client error response code indicates that the server can't find the - * requested resource. Links which lead to a 404 page are often called broken or dead links, and - * can be subject to link rot. - * A 404 status code does not indicate whether the resource is temporarily or permanently missing. - * But if a resource is permanently removed, a 410 (Gone) should be used instead of a 404 status. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static notFound = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.NOT_FOUND, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed response status code indicates - * that the request method is known by the server but is not supported by the target resource. The - * server MUST generate an Allow header field in a 405 response containing a list of the target - * resource's currently supported methods. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static methodNotAllowed = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.METHOD_NOT_ALLOWED, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 406 Not Acceptable client error response code indicates - * that the server cannot produce a response matching the list of acceptable values defined in the - * request's proactive content negotiation headers, and that the server is unwilling to supply a - * default representation. - * In practice, this error is very rarely used. Instead of responding using this error code, which - * would be cryptic for the end user and difficult to fix, servers ignore the relevant header and - * serve an actual page to the user. It is assumed that even if the user won't be completely - * happy, they will prefer this to an error code. - * If a server returns such an error status, the body of the message should contain the list of - * the available representations of the resources, allowing the user to choose among them. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static notAcceptable = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.NOT_ACCEPTABLE, options); - }; - /** - * The HTTP 407 Proxy Authentication Required client error status response code indicates that the - * request has not been applied because it lacks valid authentication credentials for a proxy - * server that is between the browser and the server that can access the requested resource. - * This status is sent with a Proxy-Authenticate header that contains information on how to - * authorize correctly. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static proxyAuthRequired = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.PROXY_AUTHENTICATION_REQUIRED, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 408 Request Timeout response status code means that the - * server would like to shut down this unused connection. It is sent on an idle connection by some - * servers, even without any previous request by the client. - * A server should send the "close" Connection header field in the response, since 408 implies - * that the server has decided to close the connection rather than continue waiting. - * This response is used much more since some browsers, like Chrome, Firefox 27+, and IE9, use - * HTTP pre-connection mechanisms to speed up surfing. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static requestTimeout = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.REQUEST_TIMEOUT, options); - }; - /** - * The HTTP 409 Conflict response status code indicates a request conflict with current state of - * the server. - * Conflicts are most likely to occur in response to a PUT request. For example, you may get a 409 - * response when uploading a file which is older than the one already on the server resulting in a - * version control conflict. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static conflict = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.CONFLICT, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 410 Gone client error response code indicates that - * access to the target resource is no longer available at the origin server and that this - * condition is likely to be permanent. - * If you don't know whether this condition is temporary or permanent, a 404 status code should be - * used instead. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static gone = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.GONE, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 411 Length Required client error response code indicates - * that the server refuses to accept the request without a defined Content-Length header. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static lengthRequired = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.LENGTH_REQUIRED, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 412 Precondition Failed client error response code - * indicates that access to the target resource has been denied. This happens with conditional - * requests on methods other than GET or HEAD when the condition defined by the - * If-Unmodified-Since or If-None-Match headers is not fulfilled. In that case, the request, - * usually an upload or a modification of a resource, cannot be made and this error response is - * sent back. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static preconditionFailed = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.PRECONDITION_FAILED, options); - }; - /** - * The HTTP 413 Payload Too Large response status code indicates that the request entity is larger - * than limits defined by server; the server might close the connection or return a Retry-After - * header field. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static payloadTooLarge = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.PAYLOAD_TOO_LARGE, options); - }; - /** - * The HTTP 414 URI Too Long response status code indicates that the URI requested by the client - * is longer than the server is willing to interpret. - * There are a few rare conditions when this might occur: - * - when a client has improperly converted a POST request to a GET request with long query - * information, - * - when the client has descended into a loop of redirection (for example, a redirected URI - * prefix that points to a suffix of itself), - * - or when the server is under attack by a client attempting to exploit potential security - * holes - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static uriTooLong = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.URI_TOO_LONG, options); - }; - /** - * The HTTP 415 Unsupported Media Type client error response code indicates that the server - * refuses to accept the request because the payload format is in an unsupported format. - * The format problem might be due to the request's indicated Content-Type or Content-Encoding, or - * as a result of inspecting the data directly. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static unsupportedMediaType = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.UNSUPPORTED_MEDIA_TYPE, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 416 Range Not Satisfiable error response code indicates - * that a server cannot serve the requested ranges. The most likely reason is that the document - * doesn't contain such ranges, or that the Range header value, though syntactically correct, - * doesn't make sense. - * The 416 response message contains a Content-Range indicating an unsatisfied range (that is a - * '*') followed by a '/' and the current length of the resource. E.g. Content-Range: bytes /12777 - * Faced with this error, browsers usually either abort the operation (for example, a download - * will be considered as non-resumable) or ask for the whole document again. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static rangeNotSatisfiable = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.RANGE_NOT_SATISFIABLE, options); - }; - /** - * The HTTP 417 Expectation Failed client error response code indicates that the expectation given - * in the request's Expect header could not be met. - * See the Expect header for more details. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static expectationFailed = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.EXPECTATION_FAILED, options); - }; - /** - * The HTTP 418 I'm a teapot client error response code indicates that the server refuses to brew - * coffee because it is, permanently, a teapot. A combined coffee/tea pot that is temporarily out - * of coffee should instead return 503. This error is a reference to Hyper Text Coffee Pot Control - * Protocol defined in April Fools' jokes in 1998 and 2014. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static teapot = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.I_AM_A_TEAPOT, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 422 Unprocessable Entity response status code indicates - * that the server understands the content type of the request entity, and the syntax of the - * request entity is correct, but it was unable to process the contained instructions. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static unprocessableEntity = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.UNPROCESSABLE_ENTITY, options); - }; - /** - * The 423 (Locked) status code means the source or destination resource of a method is locked. - * This response SHOULD contain an appropriate precondition or postcondition code, such as - * 'lock-token-submitted' or 'no-conflicting-lock'. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static locked = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.LOCKED, options); - }; - /** - * The 424 (Failed Dependency) status code means that the method could not be performed on the - * resource because the requested action depended on another action and that action failed. For - * example, if a command in a PROPPATCH method fails, then, at minimum, the rest of the commands - * will also fail with 424 (Failed Dependency). - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static failedDependency = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.FAILED_DEPENDENCY, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 425 Too Early response status code indicates that the - * server is unwilling to risk processing a request that might be replayed, which creates the - * potential for a replay attack. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static tooEarly = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.TOO_EARLY, options); - }; - /** - * The HTTP 426 Upgrade Required client error response code indicates that the server refuses to - * perform the request using the current protocol but might be willing to do so after the client - * upgrades to a different protocol. - * The server sends an Upgrade header with this response to indicate the required protocol(s). - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static upgradeRequired = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.UPGRADE_REQUIRED, options); - }; - /** - * The HTTP 428 Precondition Required response status code indicates that the server requires the - * request to be conditional. - * Typically, this means that a required precondition header, such as If-Match, is missing. - * When a precondition header is not matching the server side state, the response should be 412 - * Precondition Failed. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static preconditionRequired = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.PRECONDITION_REQUIRED, options); - }; - /** - * The HTTP 429 Too Many Requests response status code indicates the user has sent too many - * requests in a given amount of time ("rate limiting"). - * A Retry-After header might be included to this response indicating how long to wait before - * making a new request - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static tooManyRequests = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.TOO_MANY_REQUESTS, options); - }; - /** - * The HTTP 431 Request Header Fields Too Large response status code indicates that the server - * refuses to process the request because the request’s HTTP headers are too long. The request may - * be resubmitted after reducing the size of the request headers. - * 431 can be used when the total size of request headers is too large, or when a single header - * field is too large. To help those running into this error, indicate which of the two is the - * problem in the response body — ideally, also include which headers are too large. This lets - * users attempt to fix the problem, such as by clearing their cookies. - * Servers will often produce this status if: - * - The Referer URL is too long - * - There are too many Cookies sent in the request - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static headerFieldsTooLarge = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.REQUEST_HEADER_FIELDS_TOO_LARGE, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 451 Unavailable For Legal Reasons client error response - * code indicates that the user requested a resource that is not available due to legal reasons, - * such as a web page for which a legal action has been issued. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static illegal = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.UNAVAILABLE_FOR_LEGAL_REASONS, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 500 Internal Server Error server error response code - * indicates that the server encountered an unexpected condition that prevented it from fulfilling - * the request. - * This error response is a generic "catch-all" response. Usually, this indicates the server - * cannot find a better 5xx error code to response. Sometimes, server administrators log error - * responses like the 500 status code with more details about the request to prevent the error - * from happening again in the future. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static internalServerError = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.INTERNAL_SERVER_ERROR, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 501 Not Implemented server error response code means - * that the server does not support the functionality required to fulfill the request. - * This status can also send a Retry-After header, telling the requester when to check back to see - * if the functionality is supported by then. - * 501 is the appropriate response when the server does not recognize the request method and is - * incapable of supporting it for any resource. The only methods that servers are required to - * support (and therefore that must not return 501) are GET and HEAD. - * If the server does recognize the method, but intentionally does not support it, the appropriate - * response is 405 Method Not Allowed. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static notImplemented = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.NOT_IMPLEMENTED, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 502 Bad Gateway server error response code indicates - * that the server, while acting as a gateway or proxy, received an invalid response from the - * upstream server. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static badGateway = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.BAD_GATEWAY, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code - * indicates that the server is not ready to handle the request. - * Common causes are a server that is down for maintenance or that is overloaded. This response - * should be used for temporary conditions and the Retry-After HTTP header should, if possible, - * contain the estimated time for the recovery of the service. - * Caching-related headers that are sent along with this response should be taken care of, as a - * 503 status is often a temporary condition and responses shouldn't usually be cached. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static serverUnavailable = ( - message: string, - uuid: string, - options?: BoomOptions - ): Boom => { - return regularError(message, uuid, HTTPCode.SERVICE_UNAVAILABLE, options); - }; - /** - * The HyperText Transfer Protocol (HTTP) 504 Gateway Timeout server error response code indicates - * that the server, while acting as a gateway or proxy, did not get a response in time from the - * upstream server that it needed in order to complete the request. - * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem - * @param message - Human-readable explanation specific to this occurrence of the problem - * @param options - Specific options for enhanced error management - * @public - */ - public static gatewayTimeout = (message: string, uuid: string, options?: BoomOptions): Boom => { - return regularError(message, uuid, HTTPCode.GATEWAY_TIMEOUT, options); - }; -} -function regularError(message: string, uuid: string, code: number, options?: BoomOptions): Boom { - return new Boom(message, uuid, code, options); -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. aaa + */ + +import { BoomOptions } from '../types'; +import { HTTPCode } from '../types/HTTPCode.t'; +import { Boom } from './BoomError'; + +/** + * Helpers for easy generation of Boom kind errors + * - Client error (`400`-`499`) + * - {@link badRequest | `400 Bad Request`} + * - {@link unauthorized | `401 Unauthorized`} + * - {@link paymentRequired | `402 Payment Required`} + * - {@link forbidden | `403 Forbidden`} + * - {@link notFound | `404 Not Found`} + * - {@link methodNotAllowed | `405 Method Not Allowed`} + * - {@link notAcceptable | `406 Not Acceptable`} + * - {@link proxyAuthRequired | `407 Proxy Authentication Required`} + * - {@link requestTimeout | `408 Request Timeout`} + * - {@link conflict | `409 Conflict`} + * - {@link gone | `410 Gone`} + * - {@link lengthRequired | `411 Length Required`} + * - {@link preconditionFailed | `412 Precondition Failed`} + * - {@link payloadTooLarge | `413 Payload Too Large`} + * - {@link uriTooLong | `414 URI Too Long`} + * - {@link unsupportedMediaType | `415 Unsupported Media Type`} + * - {@link rangeNotSatisfiable | `416 Range Not Satisfiable`} + * - {@link expectationFailed | `417 Expectation Failed`} + * - {@link teapot | `418 I'm a teapot`} + * - {@link unprocessableEntity | `422 Unprocessable Entity`} + * - {@link locked | `423 Locked`} + * - {@link failedDependency | `424 Failed Dependency`} + * - {@link tooEarly | `425 Too Early`} + * - {@link upgradeRequired | `426 Upgrade Required`} + * - {@link preconditionRequired | `428 Precondition Required`} + * - {@link tooManyRequests | `429 Too Many Requests`} + * - {@link headerFieldsTooLarge | `431 Request Header Fields Too Large`} + * - {@link illegal | `451 Unavailable For Legal Reasons`} + * - Server error (`500`-`599`) + * - {@link internalServerError | `500 Internal Server Error`} + * - {@link notImplemented | `501 Not Implemented`} + * - {@link badGateway | `502 Bad Gateway`} + * - {@link serverUnavailable | `503 Service Unavailable`} + * - {@link gatewayTimeout | `504 Gateway Timeout`} + * @category Boom + * @public + */ +export class BoomHelpers { + /** Private constructor */ + private constructor() {} + /** The HyperText Transfer Protocol (HTTP) 400 Bad Request response status code indicates that the + * server cannot or will not process the request due to something that is perceived to be a client + * error (e.g., malformed request syntax, invalid request message framing, or deceptive request + * routing). + * The client should not repeat this request without modification. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly badRequest = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.BAD_REQUEST, options); + }; + /** + * The HTTP 401 Unauthorized client error status response code indicates that the request has not + * been applied because it lacks valid authentication credentials for the target resource. + * This status is sent with a WWW-Authenticate header that contains information on how to + * authorize correctly. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly unauthorized = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.UNAUTHORIZED, options); + }; + /** + * The HTTP 402 Payment Required is a nonstandard client error status response code that is + * reserved for future use. + * Sometimes, this code indicates that the request can not be processed until the client makes a + * payment. Originally it was created to enable digital cash or (micro) payment systems and would + * indicate that the requested content is not available until the client makes a payment. However, + * no standard use convention exists and different entities use it in different contexts. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly paymentRequired = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.PAYMENT_REQUIRED, options); + }; + /** + * The HTTP 403 Forbidden client error status response code indicates that the server understood + * the request but refuses to authorize it. + * This status is similar to 401, but in this case, re-authenticating will make no difference. The + * access is permanently forbidden and tied to the application logic, such as insufficient rights + * to a resource. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly forbidden = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.FORBIDDEN, options); + }; + /** + * The HTTP 404 Not Found client error response code indicates that the server can't find the + * requested resource. Links which lead to a 404 page are often called broken or dead links, and + * can be subject to link rot. + * A 404 status code does not indicate whether the resource is temporarily or permanently missing. + * But if a resource is permanently removed, a 410 (Gone) should be used instead of a 404 status. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly notFound = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.NOT_FOUND, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed response status code indicates + * that the request method is known by the server but is not supported by the target resource. The + * server MUST generate an Allow header field in a 405 response containing a list of the target + * resource's currently supported methods. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly methodNotAllowed = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.METHOD_NOT_ALLOWED, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 406 Not Acceptable client error response code indicates + * that the server cannot produce a response matching the list of acceptable values defined in the + * request's proactive content negotiation headers, and that the server is unwilling to supply a + * default representation. + * In practice, this error is very rarely used. Instead of responding using this error code, which + * would be cryptic for the end user and difficult to fix, servers ignore the relevant header and + * serve an actual page to the user. It is assumed that even if the user won't be completely + * happy, they will prefer this to an error code. + * If a server returns such an error status, the body of the message should contain the list of + * the available representations of the resources, allowing the user to choose among them. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly notAcceptable = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.NOT_ACCEPTABLE, options); + }; + /** + * The HTTP 407 Proxy Authentication Required client error status response code indicates that the + * request has not been applied because it lacks valid authentication credentials for a proxy + * server that is between the browser and the server that can access the requested resource. + * This status is sent with a Proxy-Authenticate header that contains information on how to + * authorize correctly. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly proxyAuthRequired = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.PROXY_AUTHENTICATION_REQUIRED, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 408 Request Timeout response status code means that the + * server would like to shut down this unused connection. It is sent on an idle connection by some + * servers, even without any previous request by the client. + * A server should send the "close" Connection header field in the response, since 408 implies + * that the server has decided to close the connection rather than continue waiting. + * This response is used much more since some browsers, like Chrome, Firefox 27+, and IE9, use + * HTTP pre-connection mechanisms to speed up surfing. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly requestTimeout = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.REQUEST_TIMEOUT, options); + }; + /** + * The HTTP 409 Conflict response status code indicates a request conflict with current state of + * the server. + * Conflicts are most likely to occur in response to a PUT request. For example, you may get a 409 + * response when uploading a file which is older than the one already on the server resulting in a + * version control conflict. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly conflict = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.CONFLICT, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 410 Gone client error response code indicates that + * access to the target resource is no longer available at the origin server and that this + * condition is likely to be permanent. + * If you don't know whether this condition is temporary or permanent, a 404 status code should be + * used instead. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly gone = (message: string, uuid: string, options?: BoomOptions): Boom => { + return regularError(message, uuid, HTTPCode.GONE, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 411 Length Required client error response code indicates + * that the server refuses to accept the request without a defined Content-Length header. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly lengthRequired = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.LENGTH_REQUIRED, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 412 Precondition Failed client error response code + * indicates that access to the target resource has been denied. This happens with conditional + * requests on methods other than GET or HEAD when the condition defined by the + * If-Unmodified-Since or If-None-Match headers is not fulfilled. In that case, the request, + * usually an upload or a modification of a resource, cannot be made and this error response is + * sent back. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly preconditionFailed = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.PRECONDITION_FAILED, options); + }; + /** + * The HTTP 413 Payload Too Large response status code indicates that the request entity is larger + * than limits defined by server; the server might close the connection or return a Retry-After + * header field. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly payloadTooLarge = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.PAYLOAD_TOO_LARGE, options); + }; + /** + * The HTTP 414 URI Too Long response status code indicates that the URI requested by the client + * is longer than the server is willing to interpret. + * There are a few rare conditions when this might occur: + * - when a client has improperly converted a POST request to a GET request with long query + * information, + * - when the client has descended into a loop of redirection (for example, a redirected URI + * prefix that points to a suffix of itself), + * - or when the server is under attack by a client attempting to exploit potential security + * holes + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly uriTooLong = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.URI_TOO_LONG, options); + }; + /** + * The HTTP 415 Unsupported Media Type client error response code indicates that the server + * refuses to accept the request because the payload format is in an unsupported format. + * The format problem might be due to the request's indicated Content-Type or Content-Encoding, or + * as a result of inspecting the data directly. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly unsupportedMediaType = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.UNSUPPORTED_MEDIA_TYPE, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 416 Range Not Satisfiable error response code indicates + * that a server cannot serve the requested ranges. The most likely reason is that the document + * doesn't contain such ranges, or that the Range header value, though syntactically correct, + * doesn't make sense. + * The 416 response message contains a Content-Range indicating an unsatisfied range (that is a + * '*') followed by a '/' and the current length of the resource. E.g. Content-Range: bytes /12777 + * Faced with this error, browsers usually either abort the operation (for example, a download + * will be considered as non-resumable) or ask for the whole document again. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly rangeNotSatisfiable = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.RANGE_NOT_SATISFIABLE, options); + }; + /** + * The HTTP 417 Expectation Failed client error response code indicates that the expectation given + * in the request's Expect header could not be met. + * See the Expect header for more details. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly expectationFailed = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.EXPECTATION_FAILED, options); + }; + /** + * The HTTP 418 I'm a teapot client error response code indicates that the server refuses to brew + * coffee because it is, permanently, a teapot. A combined coffee/tea pot that is temporarily out + * of coffee should instead return 503. This error is a reference to Hyper Text Coffee Pot Control + * Protocol defined in April Fools' jokes in 1998 and 2014. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly teapot = (message: string, uuid: string, options?: BoomOptions): Boom => { + return regularError(message, uuid, HTTPCode.I_AM_A_TEAPOT, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 422 Unprocessable Entity response status code indicates + * that the server understands the content type of the request entity, and the syntax of the + * request entity is correct, but it was unable to process the contained instructions. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly unprocessableEntity = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.UNPROCESSABLE_ENTITY, options); + }; + /** + * The 423 (Locked) status code means the source or destination resource of a method is locked. + * This response SHOULD contain an appropriate precondition or postcondition code, such as + * 'lock-token-submitted' or 'no-conflicting-lock'. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly locked = (message: string, uuid: string, options?: BoomOptions): Boom => { + return regularError(message, uuid, HTTPCode.LOCKED, options); + }; + /** + * The 424 (Failed Dependency) status code means that the method could not be performed on the + * resource because the requested action depended on another action and that action failed. For + * example, if a command in a PROPPATCH method fails, then, at minimum, the rest of the commands + * will also fail with 424 (Failed Dependency). + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly failedDependency = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.FAILED_DEPENDENCY, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 425 Too Early response status code indicates that the + * server is unwilling to risk processing a request that might be replayed, which creates the + * potential for a replay attack. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly tooEarly = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.TOO_EARLY, options); + }; + /** + * The HTTP 426 Upgrade Required client error response code indicates that the server refuses to + * perform the request using the current protocol but might be willing to do so after the client + * upgrades to a different protocol. + * The server sends an Upgrade header with this response to indicate the required protocol(s). + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly upgradeRequired = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.UPGRADE_REQUIRED, options); + }; + /** + * The HTTP 428 Precondition Required response status code indicates that the server requires the + * request to be conditional. + * Typically, this means that a required precondition header, such as If-Match, is missing. + * When a precondition header is not matching the server side state, the response should be 412 + * Precondition Failed. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly preconditionRequired = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.PRECONDITION_REQUIRED, options); + }; + /** + * The HTTP 429 Too Many Requests response status code indicates the user has sent too many + * requests in a given amount of time ("rate limiting"). + * A Retry-After header might be included to this response indicating how long to wait before + * making a new request + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly tooManyRequests = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.TOO_MANY_REQUESTS, options); + }; + /** + * The HTTP 431 Request Header Fields Too Large response status code indicates that the server + * refuses to process the request because the request’s HTTP headers are too long. The request may + * be resubmitted after reducing the size of the request headers. + * 431 can be used when the total size of request headers is too large, or when a single header + * field is too large. To help those running into this error, indicate which of the two is the + * problem in the response body — ideally, also include which headers are too large. This lets + * users attempt to fix the problem, such as by clearing their cookies. + * Servers will often produce this status if: + * - The Referer URL is too long + * - There are too many Cookies sent in the request + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly headerFieldsTooLarge = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.REQUEST_HEADER_FIELDS_TOO_LARGE, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 451 Unavailable For Legal Reasons client error response + * code indicates that the user requested a resource that is not available due to legal reasons, + * such as a web page for which a legal action has been issued. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly illegal = (message: string, uuid: string, options?: BoomOptions): Boom => { + return regularError(message, uuid, HTTPCode.UNAVAILABLE_FOR_LEGAL_REASONS, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 500 Internal Server Error server error response code + * indicates that the server encountered an unexpected condition that prevented it from fulfilling + * the request. + * This error response is a generic "catch-all" response. Usually, this indicates the server + * cannot find a better 5xx error code to response. Sometimes, server administrators log error + * responses like the 500 status code with more details about the request to prevent the error + * from happening again in the future. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly internalServerError = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.INTERNAL_SERVER_ERROR, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 501 Not Implemented server error response code means + * that the server does not support the functionality required to fulfill the request. + * This status can also send a Retry-After header, telling the requester when to check back to see + * if the functionality is supported by then. + * 501 is the appropriate response when the server does not recognize the request method and is + * incapable of supporting it for any resource. The only methods that servers are required to + * support (and therefore that must not return 501) are GET and HEAD. + * If the server does recognize the method, but intentionally does not support it, the appropriate + * response is 405 Method Not Allowed. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly notImplemented = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.NOT_IMPLEMENTED, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 502 Bad Gateway server error response code indicates + * that the server, while acting as a gateway or proxy, received an invalid response from the + * upstream server. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly badGateway = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.BAD_GATEWAY, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code + * indicates that the server is not ready to handle the request. + * Common causes are a server that is down for maintenance or that is overloaded. This response + * should be used for temporary conditions and the Retry-After HTTP header should, if possible, + * contain the estimated time for the recovery of the service. + * Caching-related headers that are sent along with this response should be taken care of, as a + * 503 status is often a temporary condition and responses shouldn't usually be cached. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly serverUnavailable = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.SERVICE_UNAVAILABLE, options); + }; + /** + * The HyperText Transfer Protocol (HTTP) 504 Gateway Timeout server error response code indicates + * that the server, while acting as a gateway or proxy, did not get a response in time from the + * upstream server that it needed in order to complete the request. + * @param uuid - UUID V4, unique identifier for this particular occurrence of the problem + * @param message - Human-readable explanation specific to this occurrence of the problem + * @param options - Specific options for enhanced error management + * @public + */ + public static readonly gatewayTimeout = ( + message: string, + uuid: string, + options?: BoomOptions + ): Boom => { + return regularError(message, uuid, HTTPCode.GATEWAY_TIMEOUT, options); + }; +} +function regularError(message: string, uuid: string, code: number, options?: BoomOptions): Boom { + return new Boom(message, uuid, code, options); +} + diff --git a/packages/api/crash/src/Crash/CrashError.ts b/packages/api/crash/src/Crash/CrashError.ts index 238fc3c6..3dd8fddd 100644 --- a/packages/api/crash/src/Crash/CrashError.ts +++ b/packages/api/crash/src/Crash/CrashError.ts @@ -1,174 +1,174 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { v4 } from 'uuid'; -import { Base } from '../BaseError'; -import { Multi } from '../Multi'; -import { Cause, CrashObject, CrashOptions } from '../types'; - -/** - * Improved handling of standard errors. - * - * Crash helps us manage standard errors within our application by providing us with some tools: - * - Association of errors and their causes in a hierarchical way. - * - Simple search for root causes within the hierarchy of errors. - * - Stack management, both of the current instance of the error, and of the causes. - * - Facilitate error logging. - * - * In addition, in combination with the Multi error types, errors in validation processes, and Boom, - * errors for the REST-API interfaces, it allows a complete management of the different types of - * errors in our backend. - * @category Crash - * @public - */ -export class Crash extends Base { - /** Crash error cause */ - private readonly _cause?: Cause; - /** Crash error */ - private readonly _isCrash = true; - /** - * Check if an object is a valid Crash or Multi error - * @param error - error to be checked - * @param uuid - Optional uuid to be used instead of a random one. - * @returns - */ - public static from(error: unknown, uuid?: string): Multi | Crash { - if (error instanceof Crash || error instanceof Multi) { - return error; - } else if (error instanceof Error) { - return new Crash(error.message, uuid || v4(), { name: error.name }); - } else if (typeof error === 'string') { - return new Crash(error, uuid || v4()); - } else if ( - error && - typeof error === 'object' && - typeof (error as Record)['message'] === 'string' - ) { - return new Crash((error as Record)['message']); - } else { - return new Crash(`Unexpected error type`, uuid || v4(), { - info: { error }, - }); - } - } - /** - * Create a new Crash error instance - * @param message - human friendly error message - */ - constructor(message: string); - /** - * Create a new Crash error - * @param message - human friendly error message - * @param options - enhanced error options - */ - constructor(message: string, options: CrashOptions); - /** - * Create a new Crash error - * @param message - human friendly error message - * @param uuid - unique identifier for this particular occurrence of the problem - */ - constructor(message: string, uuid: string); - /** - * Create a new Crash error - * @param message - human friendly error message - * @param uuid - unique identifier for this particular occurrence of the problem - * @param options - enhanced error options - */ - constructor(message: string, uuid: string, options: CrashOptions); - constructor(message: string, uuid?: string | CrashOptions, options?: CrashOptions) { - super(message, uuid, options); - // ***************************************************************************************** - // #region options type safe - if (this._options && this._options['cause']) { - if (this._options['cause'] instanceof Crash || this._options['cause'] instanceof Error) { - this._cause = this._options['cause']; - } else { - throw new Base('Parameter cause must be an Error/Crash', uuid); - } - } - // #endregion - if (this.name === 'BaseError') { - this.name = 'CrashError'; - } - } - /** Determine if this instance is a Crash error */ - public get isCrash(): boolean { - return this._isCrash; - } - /** Cause source of error */ - public override get cause(): Cause | undefined { - return this._cause; - } - /** Get the trace of this hierarchy of errors */ - public trace(): string[] { - const trace: string[] = []; - let cause = this._cause; - while (cause) { - if (cause instanceof Multi) { - trace.push(`caused by ${cause.toString()}`); - if (cause.causes) { - trace.push(...cause.causes.map(entry => `failed with ${entry.toString()}`)); - } - cause = undefined; - } else if (cause instanceof Crash) { - trace.push(`caused by ${cause.toString()}`); - cause = cause.cause; - } else { - trace.push(`caused by ${cause.name}: ${cause.message}`); - cause = undefined; - } - } - trace.unshift(this.toString()); - return trace; - } - /** - * Look in the nested causes of the error and return the first occurrence of a cause with the - * indicated name - * @param name - name of the error to search for - * @returns the cause, if there is any present with that name - */ - public findCauseByName(name: string): Cause | undefined { - let cause = this._cause; - while (cause) { - if (cause.name === name) { - return cause; - } else if (cause instanceof Crash) { - cause = cause.cause; - } else { - return undefined; - } - } - return undefined; - } - /** - * Check if there is any cause in the stack with the indicated name - * @param name - name of the error to search for - * @returns Boolean value as the result of the search - */ - public hasCauseWithName(name: string): boolean { - return this.findCauseByName(name) !== undefined; - } - /** - * Returns a full stack of the error and causes hierarchically. The string contains the - * description of the point in the code at which the Error/Crash was instantiated - */ - public fullStack(): string | undefined { - if (this._cause instanceof Crash) { - return `${this.stack}\ncaused by ${this._cause.fullStack()}`; - } else if (this._cause instanceof Error) { - return `${this.stack}\ncaused by ${this._cause.stack}`; - } - return this.stack; - } - /** Return Crash error in JSON format */ - public toJSON(): CrashObject { - return { - ...super.toObject(), - trace: this.trace(), - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { v4 } from 'uuid'; +import { Base } from '../BaseError'; +import { Multi } from '../Multi'; +import { Cause, CrashObject, CrashOptions } from '../types'; + +/** + * Improved handling of standard errors. + * + * Crash helps us manage standard errors within our application by providing us with some tools: + * - Association of errors and their causes in a hierarchical way. + * - Simple search for root causes within the hierarchy of errors. + * - Stack management, both of the current instance of the error, and of the causes. + * - Facilitate error logging. + * + * In addition, in combination with the Multi error types, errors in validation processes, and Boom, + * errors for the REST-API interfaces, it allows a complete management of the different types of + * errors in our backend. + * @category Crash + * @public + */ +export class Crash extends Base { + /** Crash error cause */ + private readonly _cause?: Cause; + /** Crash error */ + private readonly _isCrash = true; + /** + * Check if an object is a valid Crash or Multi error + * @param error - error to be checked + * @param uuid - Optional uuid to be used instead of a random one. + * @returns + */ + public static from(error: unknown, uuid?: string): Multi | Crash { + if (error instanceof Crash || error instanceof Multi) { + return error; + } else if (error instanceof Error) { + return new Crash(error.message, uuid ?? v4(), { name: error.name }); + } else if (typeof error === 'string') { + return new Crash(error, uuid ?? v4()); + } else if ( + error && + typeof error === 'object' && + typeof (error as Record)['message'] === 'string' + ) { + return new Crash((error as Record)['message']); + } else { + return new Crash(`Unexpected error type`, uuid ?? v4(), { + info: { error }, + }); + } + } + /** + * Create a new Crash error instance + * @param message - human friendly error message + */ + constructor(message: string); + /** + * Create a new Crash error + * @param message - human friendly error message + * @param options - enhanced error options + */ + constructor(message: string, options: CrashOptions); + /** + * Create a new Crash error + * @param message - human friendly error message + * @param uuid - unique identifier for this particular occurrence of the problem + */ + constructor(message: string, uuid: string); + /** + * Create a new Crash error + * @param message - human friendly error message + * @param uuid - unique identifier for this particular occurrence of the problem + * @param options - enhanced error options + */ + constructor(message: string, uuid: string, options: CrashOptions); + constructor(message: string, uuid?: string | CrashOptions, options?: CrashOptions) { + super(message, uuid, options); + // ***************************************************************************************** + // #region options type safe + if (this._options?.['cause']) { + if (this._options['cause'] instanceof Crash || this._options['cause'] instanceof Error) { + this._cause = this._options['cause']; + } else { + throw new Base('Parameter cause must be an Error/Crash', uuid); + } + } + // #endregion + if (this.name === 'BaseError') { + this.name = 'CrashError'; + } + } + /** Determine if this instance is a Crash error */ + public get isCrash(): boolean { + return this._isCrash; + } + /** Cause source of error */ + public override get cause(): Cause | undefined { + return this._cause; + } + /** Get the trace of this hierarchy of errors */ + public trace(): string[] { + const trace: string[] = []; + let cause = this._cause; + while (cause) { + if (cause instanceof Multi) { + trace.push(`caused by ${cause.toString()}`); + if (cause.causes) { + trace.push(...cause.causes.map(entry => `failed with ${entry.toString()}`)); + } + cause = undefined; + } else if (cause instanceof Crash) { + trace.push(`caused by ${cause.toString()}`); + cause = cause.cause; + } else { + trace.push(`caused by ${cause.name}: ${cause.message}`); + cause = undefined; + } + } + trace.unshift(this.toString()); + return trace; + } + /** + * Look in the nested causes of the error and return the first occurrence of a cause with the + * indicated name + * @param name - name of the error to search for + * @returns the cause, if there is any present with that name + */ + public findCauseByName(name: string): Cause | undefined { + let cause = this._cause; + while (cause) { + if (cause.name === name) { + return cause; + } else if (cause instanceof Crash) { + cause = cause.cause; + } else { + return undefined; + } + } + return undefined; + } + /** + * Check if there is any cause in the stack with the indicated name + * @param name - name of the error to search for + * @returns Boolean value as the result of the search + */ + public hasCauseWithName(name: string): boolean { + return this.findCauseByName(name) !== undefined; + } + /** + * Returns a full stack of the error and causes hierarchically. The string contains the + * description of the point in the code at which the Error/Crash was instantiated + */ + public fullStack(): string | undefined { + if (this._cause instanceof Crash) { + return `${this.stack}\ncaused by ${this._cause.fullStack()}`; + } else if (this._cause instanceof Error) { + return `${this.stack}\ncaused by ${this._cause.stack}`; + } + return this.stack; + } + /** Return Crash error in JSON format */ + public toJSON(): CrashObject { + return { + ...super.toObject(), + trace: this.trace(), + }; + } +} diff --git a/packages/api/crash/src/Multi/MultiError.test.ts b/packages/api/crash/src/Multi/MultiError.test.ts index f04f8750..24f52cb2 100644 --- a/packages/api/crash/src/Multi/MultiError.test.ts +++ b/packages/api/crash/src/Multi/MultiError.test.ts @@ -1,351 +1,352 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { v4 } from 'uuid'; -import { Crash } from '../Crash/CrashError'; -import { CONFIG_MAX_ERROR_MESSAGE_LENGTH } from '../const'; -import { Multi } from './MultiError'; -const uuidTest = v4(); -const causes: Array = []; -for (let i = 0; i < 5; i++) { - causes.push(new Crash('Crash Error', uuidTest, { name: 'ValidationError' })); -} -describe('In #Multi class the ', () => { - describe('constructor ', () => { - it('Should create an instance with (message) parameter such that: name=Multi, cause=undefined, info=undefined, message=Example', () => { - const errorTest = new Multi('Example', uuidTest); - expect(errorTest.name).toEqual('MultiError'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes).toBeUndefined(); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, name) parameter such that: name=ERROR_TYPE, cause=undefined, info=undefined, message=Example', () => { - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes).toBeUndefined(); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, name, info) parameter such that: name=ERROR_TYPE, cause=undefined, info=objectTest, message=Example', () => { - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - info: objectTest, - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes).toBeUndefined(); - expect(errorTest.info).toEqual(objectTest); - }); - it('Should create an instance with (message, name, error) parameter such that: name=ERROR_TYPE, cause=Cause, info=undefined, message=Example', () => { - const cause = new Error('Cause'); - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - causes: cause, - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes?.length).toEqual(1); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, name, causes) parameter such that: name=ERROR_TYPE, cause=cause[0], causes= causes, info=undefined, message=Example', () => { - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - causes, - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes?.length).toEqual(5); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(causes[0]); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, name, error, info) parameter such that: name=ERROR_TYPE, cause=Cause, info=objectTest, message=Example', () => { - const cause = new Error('Cause'); - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - causes: cause, - info: objectTest, - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.message).toEqual('Example'); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(cause); - expect(errorTest.info).toEqual(objectTest); - }); - it('Should create an instance with (message, name, causes, info) parameter such that: name=ERROR_TYPE, cause=cause[0], causes=causes, info=objectTest, message=Example', () => { - const cause = new Error('Cause'); - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorTest = new Multi('Example', uuidTest, { - name: 'ERROR_TYPE', - causes, - info: objectTest, - }); - expect(errorTest.name).toEqual('ERROR_TYPE'); - expect(errorTest.causes?.length).toEqual(5); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(causes[0]); - expect(errorTest.info).toEqual(objectTest); - }); - it('Should create an instance with (message, error) parameter such that: name=Crash, cause=Cause, info=undefined, message=Example', () => { - const cause = new Crash('Cause', uuidTest); - const errorTest = new Multi('Example', uuidTest, { causes: cause }); - expect(errorTest.name).toEqual('MultiError'); - expect(errorTest.message).toEqual('Example'); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(cause); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, causes) parameter such that: name=Crash, cause=cause[0], causes=causes, info=undefined, message=Example', () => { - const errorTest = new Multi('Example', uuidTest, { causes }); - expect(errorTest.name).toEqual('MultiError'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes?.length).toEqual(5); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(causes[0]); - expect(errorTest.info).toBeUndefined(); - }); - it('Should create an instance with (message, error, info) parameter such that: name=Crash, cause=Cause, info=objectTest, message=Example', () => { - const cause = new Crash('Cause', uuidTest); - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorTest = new Multi('Example', uuidTest, { - causes: cause, - info: objectTest, - }); - expect(errorTest.name).toEqual('MultiError'); - expect(errorTest.message).toEqual('Example'); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(cause); - expect(errorTest.info).toEqual(objectTest); - }); - it('Should create an instance with (message, causes, info) parameter such that: name=Crash, cause=cases[0], causes=causes, info=objectTest, message=Example', () => { - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorTest = new Multi('Example', uuidTest, { - causes, - info: objectTest, - }); - expect(errorTest.name).toEqual('MultiError'); - expect(errorTest.message).toEqual('Example'); - expect(errorTest.causes?.length).toEqual(5); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toEqual(causes[0]); - expect(errorTest.info).toEqual(objectTest); - }); - it('Should throw a Crash error if message!=string', () => { - const test = () => { - //@ts-ignore - Test environment - new Multi(5, uuidTest); - }; - expect(test).toThrowError('Message parameter must be a string'); - }); - it(`Should truncate the message if message is to large (>${CONFIG_MAX_ERROR_MESSAGE_LENGTH})`, () => { - const error = new Crash('o'.padEnd(CONFIG_MAX_ERROR_MESSAGE_LENGTH + 1, 'o'), uuidTest); - expect(error.message.length).toEqual(CONFIG_MAX_ERROR_MESSAGE_LENGTH); - expect(error.message).toContain('...too long error'); - }); - it('Should throw a Crash error if name!=string', () => { - const test = () => { - //@ts-ignore - Test environment - new Multi('Error', uuidTest, { name: 5 }); - }; - expect(test).toThrowError('Parameter name must a string'); - }); - it('Should throw a Crash error if the UUID is not valid', () => { - const test = () => { - //@ts-ignore - Test environment - new Multi('Error', 5, new Error(), {}, 'tooMuch'); - }; - expect(test).toThrowError('uuid parameter must be an string and RFC 4122 based'); - }); - it('Should throw a Crash error if the causes is not an Array', () => { - const test = () => { - //@ts-ignore - Test environment - new Multi('Error', uuidTest, { causes: 0 }); - }; - expect(test).toThrowError('Options[causes] must be an array of Error/Crash'); - }); - it('Should throw a Crash error if the causes are not error or Crash', () => { - //@ts-ignore - Test environment - causes.push(5); - const test = () => { - new Multi('Error', uuidTest, { causes }); - }; - expect(test).toThrowError('Options[causes] must be an array of Error/Crash'); - causes.pop(); - }); - }); - describe('methods ', () => { - const query = { query: 'fake' }; - const request = { request: 'fake' }; - const endpoint = { method: 'get' }; - const controllerCrashError = new Multi('Getting', uuidTest, { - name: 'ControllerError', - causes, - info: endpoint, - }); - beforeAll(done => { - causes.push(new Error('Regular Error')); - done(); - }); - afterAll(done => { - causes.pop(); - done(); - }); - it('isMulti return true', () => { - const errorTest = new Multi('Example', uuidTest); - expect(errorTest.isMulti).toBeTruthy(); - }); - it('uuid return the uuid', () => { - const errorTest = new Multi('Example', uuidTest); - expect(errorTest.uuid).toEqual(uuidTest); - }); - it('causes return undefined if there is no cause', () => { - const errorTest = new Multi('Example', uuidTest); - expect(errorTest.causes).toBeUndefined(); - }); - it('causes return an Error if there is the cause is an error', () => { - const errorCause = new Error('Cause'); - const errorTest = new Multi('Example', uuidTest, { causes: errorCause }); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toBeInstanceOf(Error); - //@ts-ignore - Test environment - expect(errorTest.causes[0].name).toEqual('Error'); - //@ts-ignore - Test environment - expect(errorTest.causes[0].message).toEqual('Cause'); - }); - it('causes return an Error if there is the cause is an error created by causes', () => { - const errorTest = new Multi('Example', uuidTest, { causes }); - //@ts-ignore - Test environment - expect(errorTest.causes[0]).toBeInstanceOf(Crash); - //@ts-ignore - Test environment - expect(errorTest.causes[0].name).toEqual('ValidationError'); - //@ts-ignore - Test environment - expect(errorTest.causes[0].message).toEqual('Crash Error'); - }); - it('info return undefined if there is no info', () => { - const errorCause = new Error('Cause'); - const errorTest = new Multi('Example', uuidTest, { causes: errorCause }); - expect(errorTest.info).toBeUndefined(); - }); - it('info return info if there is info', () => { - const objectTest = { - par1: 'info1', - par2: 'info2', - }; - const errorCause = new Error('Cause'); - const errorTest = new Multi('Example', uuidTest, { - causes: errorCause, - info: objectTest, - }); - expect(errorTest.info).toEqual(objectTest); - }); - it('toString() return a string with "name: message"', () => { - const errorTest = new Multi('Example', uuidTest); - expect(errorTest.toString()).toEqual('MultiError: Example'); - }); - it('trace() return a string with "name: message; caused by name: message; caused by ..."', () => { - const str = [ - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'Error: Regular Error', - ]; - expect(controllerCrashError.trace()).toEqual(str); - }); - it('findCauseByName() should find a cause when this cause exists', () => { - const cause = controllerCrashError.findCauseByName('ValidationError'); - expect(cause).toBeInstanceOf(Crash); - expect(cause?.name).toEqual('ValidationError'); - expect(cause?.message).toEqual('Crash Error'); - }); - it('findCauseByName() should not find a cause when this cause no exists in a long nested chain', () => { - const cause = controllerCrashError.findCauseByName('no'); - expect(cause).toBeUndefined(); - }); - it('findCauseByName() should not find a cause when this cause no exists in a single error', () => { - const errorTest = new Multi('Error', uuidTest); - const cause = errorTest.findCauseByName('no'); - expect(cause).toBeUndefined(); - }); - it('hasCauseWithName() should return a true when try to find a cause and this cause exists', () => { - const cause = controllerCrashError.hasCauseWithName('ValidationError'); - expect(cause).toBeTruthy(); - }); - it('hasCauseWithName() should return a false when try to find a cause and this cause exists', () => { - const cause = controllerCrashError.hasCauseWithName('no'); - expect(cause).toBeFalsy(); - }); - it('fullStack() should return the complete trace of the error sequence in a long nested chain', () => { - const stack = controllerCrashError.fullStack(); - expect(stack).toContain('ControllerError'); - expect(stack).toContain('ValidationError'); - }); - it('fullStack() should return the complete trace of the error sequence in a single error', () => { - const errorTest = new Multi('Error', uuidTest); - const stack = errorTest.fullStack(); - expect(stack).toContain('MultiError'); - }); - it('push() should add a new error to causes', () => { - const errorTest = new Multi('Error', uuidTest, { causes }); - errorTest.push(new Error('Regular Error')); - expect(errorTest.causes?.length).toEqual(7); - }); - it('pop() should remove a error in causes', () => { - const errorTest = new Multi('Error', uuidTest, { causes }); - const dropError = errorTest.pop(); - expect(errorTest.causes?.length).toEqual(6); - expect(dropError).toBeInstanceOf(Error); - }); - it('toJSON() should return a JSON well formatted', () => { - const errorTest = new Multi('Error', uuidTest, { causes }); - const erroObject = errorTest.toJSON(); - expect(erroObject).toEqual({ - name: 'MultiError', - message: 'Error', - trace: [ - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'ValidationError: Crash Error', - 'Error: Regular Error', - ], - timestamp: errorTest.date.toISOString(), - subject: 'common', - uuid: uuidTest, - info: undefined, - }); - }); - it('size should return the size of a multi error', () => { - const errorTestZero = new Multi('Error', uuidTest); - const errorTestOne = new Multi('Error', uuidTest); - errorTestOne.push(new Error()); - expect(errorTestOne.size).toEqual(1); - expect(errorTestZero.size).toEqual(0); - }); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { v4 } from 'uuid'; +import { Crash } from '../Crash/CrashError'; +import { CONFIG_MAX_ERROR_MESSAGE_LENGTH } from '../const'; +import { Multi } from './MultiError'; +const uuidTest = v4(); +const causes: Array = []; +for (let i = 0; i < 5; i++) { + causes.push(new Crash('Crash Error', uuidTest, { name: 'ValidationError' })); +} +describe('In #Multi class the ', () => { + describe('constructor ', () => { + it('Should create an instance with (message) parameter such that: name=Multi, cause=undefined, info=undefined, message=Example', () => { + const errorTest = new Multi('Example', uuidTest); + expect(errorTest.name).toEqual('MultiError'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes).toBeUndefined(); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, name) parameter such that: name=ERROR_TYPE, cause=undefined, info=undefined, message=Example', () => { + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes).toBeUndefined(); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, name, info) parameter such that: name=ERROR_TYPE, cause=undefined, info=objectTest, message=Example', () => { + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + info: objectTest, + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes).toBeUndefined(); + expect(errorTest.info).toEqual(objectTest); + }); + it('Should create an instance with (message, name, error) parameter such that: name=ERROR_TYPE, cause=Cause, info=undefined, message=Example', () => { + const cause = new Error('Cause'); + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + causes: cause, + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes?.length).toEqual(1); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, name, causes) parameter such that: name=ERROR_TYPE, cause=cause[0], causes= causes, info=undefined, message=Example', () => { + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + causes, + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes?.length).toEqual(5); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(causes[0]); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, name, error, info) parameter such that: name=ERROR_TYPE, cause=Cause, info=objectTest, message=Example', () => { + const cause = new Error('Cause'); + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + causes: cause, + info: objectTest, + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.message).toEqual('Example'); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(cause); + expect(errorTest.info).toEqual(objectTest); + }); + it('Should create an instance with (message, name, causes, info) parameter such that: name=ERROR_TYPE, cause=cause[0], causes=causes, info=objectTest, message=Example', () => { + const cause = new Error('Cause'); + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorTest = new Multi('Example', uuidTest, { + name: 'ERROR_TYPE', + causes, + info: objectTest, + }); + expect(errorTest.name).toEqual('ERROR_TYPE'); + expect(errorTest.causes?.length).toEqual(5); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(causes[0]); + expect(errorTest.info).toEqual(objectTest); + }); + it('Should create an instance with (message, error) parameter such that: name=Crash, cause=Cause, info=undefined, message=Example', () => { + const cause = new Crash('Cause', uuidTest); + const errorTest = new Multi('Example', uuidTest, { causes: cause }); + expect(errorTest.name).toEqual('MultiError'); + expect(errorTest.message).toEqual('Example'); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(cause); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, causes) parameter such that: name=Crash, cause=cause[0], causes=causes, info=undefined, message=Example', () => { + const errorTest = new Multi('Example', uuidTest, { causes }); + expect(errorTest.name).toEqual('MultiError'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes?.length).toEqual(5); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(causes[0]); + expect(errorTest.info).toBeUndefined(); + }); + it('Should create an instance with (message, error, info) parameter such that: name=Crash, cause=Cause, info=objectTest, message=Example', () => { + const cause = new Crash('Cause', uuidTest); + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorTest = new Multi('Example', uuidTest, { + causes: cause, + info: objectTest, + }); + expect(errorTest.name).toEqual('MultiError'); + expect(errorTest.message).toEqual('Example'); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(cause); + expect(errorTest.info).toEqual(objectTest); + }); + it('Should create an instance with (message, causes, info) parameter such that: name=Crash, cause=cases[0], causes=causes, info=objectTest, message=Example', () => { + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorTest = new Multi('Example', uuidTest, { + causes, + info: objectTest, + }); + expect(errorTest.name).toEqual('MultiError'); + expect(errorTest.message).toEqual('Example'); + expect(errorTest.causes?.length).toEqual(5); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toEqual(causes[0]); + expect(errorTest.info).toEqual(objectTest); + }); + it('Should throw a Crash error if message!=string', () => { + const test = () => { + //@ts-ignore - Test environment + new Multi(5, uuidTest); + }; + expect(test).toThrowError('Message parameter must be a string'); + }); + it(`Should truncate the message if message is to large (>${CONFIG_MAX_ERROR_MESSAGE_LENGTH})`, () => { + const error = new Crash('o'.padEnd(CONFIG_MAX_ERROR_MESSAGE_LENGTH + 1, 'o'), uuidTest); + expect(error.message.length).toEqual(CONFIG_MAX_ERROR_MESSAGE_LENGTH); + expect(error.message).toContain('...too long error'); + }); + it('Should throw a Crash error if name!=string', () => { + const test = () => { + //@ts-ignore - Test environment + new Multi('Error', uuidTest, { name: 5 }); + }; + expect(test).toThrowError('Parameter name must a string'); + }); + it('Should throw a Crash error if the UUID is not valid', () => { + const test = () => { + //@ts-ignore - Test environment + new Multi('Error', 5, new Error(), {}, 'tooMuch'); + }; + expect(test).toThrowError('uuid parameter must be an string and RFC 4122 based'); + }); + it('Should throw a Crash error if the causes is not an Array', () => { + const test = () => { + //@ts-ignore - Test environment + new Multi('Error', uuidTest, { causes: 1 }); + }; + expect(test).toThrowError('Options[causes] must be an array of Error/Crash'); + }); + it('Should throw a Crash error if the causes are not error or Crash', () => { + //@ts-ignore - Test environment + causes.push(5); + const test = () => { + new Multi('Error', uuidTest, { causes }); + }; + expect(test).toThrowError('Options[causes] must be an array of Error/Crash'); + causes.pop(); + }); + }); + describe('methods ', () => { + const query = { query: 'fake' }; + const request = { request: 'fake' }; + const endpoint = { method: 'get' }; + const controllerCrashError = new Multi('Getting', uuidTest, { + name: 'ControllerError', + causes, + info: endpoint, + }); + beforeAll(done => { + causes.push(new Error('Regular Error')); + done(); + }); + afterAll(done => { + causes.pop(); + done(); + }); + it('isMulti return true', () => { + const errorTest = new Multi('Example', uuidTest); + expect(errorTest.isMulti).toBeTruthy(); + }); + it('uuid return the uuid', () => { + const errorTest = new Multi('Example', uuidTest); + expect(errorTest.uuid).toEqual(uuidTest); + }); + it('causes return undefined if there is no cause', () => { + const errorTest = new Multi('Example', uuidTest); + expect(errorTest.causes).toBeUndefined(); + }); + it('causes return an Error if there is the cause is an error', () => { + const errorCause = new Error('Cause'); + const errorTest = new Multi('Example', uuidTest, { causes: errorCause }); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toBeInstanceOf(Error); + //@ts-ignore - Test environment + expect(errorTest.causes[0].name).toEqual('Error'); + //@ts-ignore - Test environment + expect(errorTest.causes[0].message).toEqual('Cause'); + }); + it('causes return an Error if there is the cause is an error created by causes', () => { + const errorTest = new Multi('Example', uuidTest, { causes }); + //@ts-ignore - Test environment + expect(errorTest.causes[0]).toBeInstanceOf(Crash); + //@ts-ignore - Test environment + expect(errorTest.causes[0].name).toEqual('ValidationError'); + //@ts-ignore - Test environment + expect(errorTest.causes[0].message).toEqual('Crash Error'); + }); + it('info return undefined if there is no info', () => { + const errorCause = new Error('Cause'); + const errorTest = new Multi('Example', uuidTest, { causes: errorCause }); + expect(errorTest.info).toBeUndefined(); + }); + it('info return info if there is info', () => { + const objectTest = { + par1: 'info1', + par2: 'info2', + }; + const errorCause = new Error('Cause'); + const errorTest = new Multi('Example', uuidTest, { + causes: errorCause, + info: objectTest, + }); + expect(errorTest.info).toEqual(objectTest); + }); + it('toString() return a string with "name: message"', () => { + const errorTest = new Multi('Example', uuidTest); + expect(errorTest.toString()).toEqual('MultiError: Example'); + }); + it('trace() return a string with "name: message; caused by name: message; caused by ..."', () => { + const str = [ + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'Error: Regular Error', + ]; + expect(controllerCrashError.trace()).toEqual(str); + }); + it('findCauseByName() should find a cause when this cause exists', () => { + const cause = controllerCrashError.findCauseByName('ValidationError'); + expect(cause).toBeInstanceOf(Crash); + expect(cause?.name).toEqual('ValidationError'); + expect(cause?.message).toEqual('Crash Error'); + }); + it('findCauseByName() should not find a cause when this cause no exists in a long nested chain', () => { + const cause = controllerCrashError.findCauseByName('no'); + expect(cause).toBeUndefined(); + }); + it('findCauseByName() should not find a cause when this cause no exists in a single error', () => { + const errorTest = new Multi('Error', uuidTest); + const cause = errorTest.findCauseByName('no'); + expect(cause).toBeUndefined(); + }); + it('hasCauseWithName() should return a true when try to find a cause and this cause exists', () => { + const cause = controllerCrashError.hasCauseWithName('ValidationError'); + expect(cause).toBeTruthy(); + }); + it('hasCauseWithName() should return a false when try to find a cause and this cause exists', () => { + const cause = controllerCrashError.hasCauseWithName('no'); + expect(cause).toBeFalsy(); + }); + it('fullStack() should return the complete trace of the error sequence in a long nested chain', () => { + const stack = controllerCrashError.fullStack(); + expect(stack).toContain('ControllerError'); + expect(stack).toContain('ValidationError'); + }); + it('fullStack() should return the complete trace of the error sequence in a single error', () => { + const errorTest = new Multi('Error', uuidTest); + const stack = errorTest.fullStack(); + expect(stack).toContain('MultiError'); + }); + it('push() should add a new error to causes', () => { + const errorTest = new Multi('Error', uuidTest, { causes }); + errorTest.push(new Error('Regular Error')); + expect(errorTest.causes?.length).toEqual(7); + }); + it('pop() should remove a error in causes', () => { + const errorTest = new Multi('Error', uuidTest, { causes }); + const dropError = errorTest.pop(); + expect(errorTest.causes?.length).toEqual(6); + expect(dropError).toBeInstanceOf(Error); + }); + it('toJSON() should return a JSON well formatted', () => { + const errorTest = new Multi('Error', uuidTest, { causes }); + const erroObject = errorTest.toJSON(); + expect(erroObject).toEqual({ + name: 'MultiError', + message: 'Error', + trace: [ + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'ValidationError: Crash Error', + 'Error: Regular Error', + ], + timestamp: errorTest.date.toISOString(), + subject: 'common', + uuid: uuidTest, + info: undefined, + }); + }); + it('size should return the size of a multi error', () => { + const errorTestZero = new Multi('Error', uuidTest); + const errorTestOne = new Multi('Error', uuidTest); + errorTestOne.push(new Error()); + expect(errorTestOne.size).toEqual(1); + expect(errorTestZero.size).toEqual(0); + }); + }); +}); + diff --git a/packages/api/crash/src/Multi/MultiError.ts b/packages/api/crash/src/Multi/MultiError.ts index c49609a7..b3b76a38 100644 --- a/packages/api/crash/src/Multi/MultiError.ts +++ b/packages/api/crash/src/Multi/MultiError.ts @@ -1,215 +1,216 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Cause } from '..'; -import { Base } from '../BaseError'; -import { Crash } from '../Crash'; -import type { MultiObject, MultiOptions, ValidationError } from '../types'; - -/** - * Improved handling of validation errors. - * - * Multi helps us to manage validation or information transformation errors, in other words, it - * helps us manage any process that may generate multiple non-hierarchical errors (an error is not a - * direct consequence of the previous one) by providing us with some tools: - * - Management of the error stack. - * - Simple search for root causes within the error stack. - * - Stack management, both of the current instance of the error, and of the causes. - * - Facilitate error logging. - * - * Furthermore, in combination with the types of error Boom, errors for the REST-API interfaces, and - * Crash, standard application errors, it allows a complete management of the different types of - * errors in our backend. - * @category Multi - * @public - */ -export class Multi extends Base { - /** Multi error causes */ - private _causes?: Cause[]; - /** Multi error */ - private readonly _isMulti = true; - /** - * Create a new Multi error - * @param message - human friendly error message - */ - constructor(message: string); - /** - * Create a new Multi error - * @param message - human friendly error message - * @param options - enhanced error options - */ - constructor(message: string, options: MultiOptions); - /** - * Create a new Multi error - * @param message - human friendly error message - * @param uuid - unique identifier for this particular occurrence of the problem - */ - constructor(message: string, uuid: string); - /** - * Create a new Multi error - * @param message - human friendly error message - * @param uuid - unique identifier for this particular occurrence of the problem - * @param options - enhanced error options - */ - constructor(message: string, uuid: string, options: MultiOptions); - constructor(message: string, uuidOrOptions?: string | MultiOptions, options?: MultiOptions) { - super(message, uuidOrOptions, options); - this._causes = this.extractCauses(this._uuid, this._options); - if (this.name === 'BaseError') { - this.name = 'MultiError'; - } - } - /** - * Extract the causes from the options - * @param uuid - unique identifier for this particular occurrence of the problem - * enhanced error options - * @param options - enhanced error options - * @returns - */ - private extractCauses(uuid: string, options?: MultiOptions): Cause[] | undefined { - if (!options || options['causes'] === undefined) { - return; - } - const causes = options['causes']; - if (!(causes instanceof Crash || causes instanceof Error) && !Array.isArray(causes)) { - throw new Base('Options[causes] must be an array of Error/Crash', uuid); - } - if (causes instanceof Crash || causes instanceof Error) { - return [causes]; - } - for (const cause of causes) { - if (!(cause instanceof Crash || cause instanceof Error)) { - throw new Base('Options[causes] must be an array of Error/Crash', uuid); - } - } - return causes; - } - /** Determine if this instance is a Multi error */ - get isMulti(): boolean { - return this._isMulti; - } - /** Causes source of error */ - get causes(): Cause[] | undefined { - return this._causes; - } - /** Return the number of causes of this error */ - get size(): number { - if (this._causes) { - return this._causes.length; - } else { - return 0; - } - } - /** Get the trace of this hierarchy of errors */ - public trace(): string[] { - const trace: string[] = []; - if (this.causes) { - this.causes.forEach(cause => { - if (cause instanceof Crash) { - trace.push(...cause.trace()); - } else { - trace.push(`${cause.name}: ${cause.message}`); - } - }); - } - return trace; - } - /** - * Look in the nested causes of the error and return the first occurrence of a cause with the - * indicated name - * @param name - name of the error to search for - * @returns the cause, if there is any present with that name - */ - public findCauseByName(name: string): Cause | undefined { - let foundCause: Cause | undefined; - if (this._causes !== undefined) { - this._causes.forEach(cause => { - if (cause.name === name && foundCause === undefined) { - foundCause = cause; - } - if (cause instanceof Crash && foundCause === undefined) { - foundCause = cause.findCauseByName(name); - } - }); - } - return foundCause; - } - /** - * Check if there is any cause in the stack with the indicated name - * @param name - name of the error to search for - * @returns Boolean value as the result of the search - */ - public hasCauseWithName(name: string): boolean { - return this.findCauseByName(name) !== undefined; - } - /** - * Returns a full stack of the error and causes hierarchically. The string contains the - * description of the point in the code at which the Error/Crash/Multi was instantiated - */ - public fullStack(): string | undefined { - let arrayStack = ''; - if (this._causes !== undefined && this._causes.length > 0) { - arrayStack += '\ncaused by '; - this._causes.forEach(cause => { - if (cause instanceof Crash) { - arrayStack += `\n[${cause.fullStack()}]`; - } else if (cause instanceof Error) { - arrayStack += `\n[${cause.stack}]`; - } - }); - } - return this.stack + arrayStack; - } - /** - * Add a new error on the array of causes - * @param error - Cause to be added to the array of causes - */ - public push(error: Cause): void { - if (this._causes !== undefined) { - this._causes.push(error); - } else { - this._causes = [error]; - } - } - /** - * Remove a error from the array of causes - * @returns the cause that have been removed - */ - public pop(): Cause | undefined { - if (this._causes !== undefined) { - return this._causes.pop(); - } else { - return undefined; - } - } - /** Return Multi error in JSON format */ - public toJSON(): MultiObject { - return { - ...super.toObject(), - trace: this.trace(), - }; - } - /** - * Process the errors thrown by Joi into the cause array - * @param error - `ValidationError` from a Joi validation process - * @returns number or error that have been introduced - */ - public Multify(error: ValidationError): number { - if (error.name === 'ValidationError') { - error.details.forEach(detail => { - this.push( - new Crash(detail.message, this._uuid, { - name: 'ValidationError', - info: detail, - }) - ); - }); - return error.details.length; - } else { - return 0; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Cause } from '..'; +import { Base } from '../BaseError'; +import { Crash } from '../Crash'; +import type { MultiObject, MultiOptions, ValidationError } from '../types'; + +/** + * Improved handling of validation errors. + * + * Multi helps us to manage validation or information transformation errors, in other words, it + * helps us manage any process that may generate multiple non-hierarchical errors (an error is not a + * direct consequence of the previous one) by providing us with some tools: + * - Management of the error stack. + * - Simple search for root causes within the error stack. + * - Stack management, both of the current instance of the error, and of the causes. + * - Facilitate error logging. + * + * Furthermore, in combination with the types of error Boom, errors for the REST-API interfaces, and + * Crash, standard application errors, it allows a complete management of the different types of + * errors in our backend. + * @category Multi + * @public + */ +export class Multi extends Base { + /** Multi error causes */ + private _causes?: Cause[]; + /** Multi error */ + private readonly _isMulti = true; + /** + * Create a new Multi error + * @param message - human friendly error message + */ + constructor(message: string); + /** + * Create a new Multi error + * @param message - human friendly error message + * @param options - enhanced error options + */ + constructor(message: string, options: MultiOptions); + /** + * Create a new Multi error + * @param message - human friendly error message + * @param uuid - unique identifier for this particular occurrence of the problem + */ + constructor(message: string, uuid: string); + /** + * Create a new Multi error + * @param message - human friendly error message + * @param uuid - unique identifier for this particular occurrence of the problem + * @param options - enhanced error options + */ + constructor(message: string, uuid: string, options: MultiOptions); + constructor(message: string, uuidOrOptions?: string | MultiOptions, options?: MultiOptions) { + super(message, uuidOrOptions, options); + this._causes = this.extractCauses(this._uuid, this._options); + if (this.name === 'BaseError') { + this.name = 'MultiError'; + } + } + /** + * Extract the causes from the options + * @param uuid - unique identifier for this particular occurrence of the problem + * enhanced error options + * @param options - enhanced error options + * @returns + */ + private extractCauses(uuid: string, options?: MultiOptions): Cause[] | undefined { + if (!options?.['causes']) { + return; + } + const causes = options['causes']; + if (!(causes instanceof Crash || causes instanceof Error) && !Array.isArray(causes)) { + throw new Base('Options[causes] must be an array of Error/Crash', uuid); + } + if (causes instanceof Crash || causes instanceof Error) { + return [causes]; + } + for (const cause of causes) { + if (!(cause instanceof Crash || cause instanceof Error)) { + throw new Base('Options[causes] must be an array of Error/Crash', uuid); + } + } + return causes; + } + /** Determine if this instance is a Multi error */ + get isMulti(): boolean { + return this._isMulti; + } + /** Causes source of error */ + get causes(): Cause[] | undefined { + return this._causes; + } + /** Return the number of causes of this error */ + get size(): number { + if (this._causes) { + return this._causes.length; + } else { + return 0; + } + } + /** Get the trace of this hierarchy of errors */ + public trace(): string[] { + const trace: string[] = []; + if (this.causes) { + this.causes.forEach(cause => { + if (cause instanceof Crash) { + trace.push(...cause.trace()); + } else { + trace.push(`${cause.name}: ${cause.message}`); + } + }); + } + return trace; + } + /** + * Look in the nested causes of the error and return the first occurrence of a cause with the + * indicated name + * @param name - name of the error to search for + * @returns the cause, if there is any present with that name + */ + public findCauseByName(name: string): Cause | undefined { + let foundCause: Cause | undefined; + if (this._causes !== undefined) { + this._causes.forEach(cause => { + if (cause.name === name && foundCause === undefined) { + foundCause = cause; + } + if (cause instanceof Crash && foundCause === undefined) { + foundCause = cause.findCauseByName(name); + } + }); + } + return foundCause; + } + /** + * Check if there is any cause in the stack with the indicated name + * @param name - name of the error to search for + * @returns Boolean value as the result of the search + */ + public hasCauseWithName(name: string): boolean { + return this.findCauseByName(name) !== undefined; + } + /** + * Returns a full stack of the error and causes hierarchically. The string contains the + * description of the point in the code at which the Error/Crash/Multi was instantiated + */ + public fullStack(): string | undefined { + let arrayStack = ''; + if (this._causes !== undefined && this._causes.length > 0) { + arrayStack += '\ncaused by '; + this._causes.forEach(cause => { + if (cause instanceof Crash) { + arrayStack += `\n[${cause.fullStack()}]`; + } else if (cause instanceof Error) { + arrayStack += `\n[${cause.stack}]`; + } + }); + } + return this.stack + arrayStack; + } + /** + * Add a new error on the array of causes + * @param error - Cause to be added to the array of causes + */ + public push(error: Cause): void { + if (this._causes !== undefined) { + this._causes.push(error); + } else { + this._causes = [error]; + } + } + /** + * Remove a error from the array of causes + * @returns the cause that have been removed + */ + public pop(): Cause | undefined { + if (this._causes !== undefined) { + return this._causes.pop(); + } else { + return undefined; + } + } + /** Return Multi error in JSON format */ + public toJSON(): MultiObject { + return { + ...super.toObject(), + trace: this.trace(), + }; + } + /** + * Process the errors thrown by Joi into the cause array + * @param error - `ValidationError` from a Joi validation process + * @returns number or error that have been introduced + */ + public Multify(error: ValidationError): number { + if (error.name === 'ValidationError') { + error.details.forEach(detail => { + this.push( + new Crash(detail.message, this._uuid, { + name: 'ValidationError', + info: detail, + }) + ); + }); + return error.details.length; + } else { + return 0; + } + } +} + diff --git a/packages/api/doorkeeper/README.md b/packages/api/doorkeeper/README.md index 72338306..a435bbb8 100644 --- a/packages/api/doorkeeper/README.md +++ b/packages/api/doorkeeper/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -118,7 +119,7 @@ const myNewAddress = await checker.validate('Address', address); // myNewAddress ## **API** -- {@link Doorkeeper} +- {@link DoorKeeper} ## **License** diff --git a/packages/api/doorkeeper/package.json b/packages/api/doorkeeper/package.json index 7f9e6914..58afc13a 100644 --- a/packages/api/doorkeeper/package.json +++ b/packages/api/doorkeeper/package.json @@ -1,57 +1,56 @@ -{ - "name": "@mdf.js/doorkeeper", - "version": "0.0.1", - "description": "MMS - API - Doorkeeper", - "keywords": [ - "NodeJS", - "MMS", - "API", - "AJV" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/doorkeeper" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "doc": "typedoc --options typedoc.json", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/doorkeeper/licenses.csv --customPath ../../../.config/customFormat.json", - "lint": "eslint \"src/**/*.ts\" --quiet --fix", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/crash": "*", - "ajv": "^8.17.1", - "ajv-errors": "^3.0.0", - "ajv-formats": "^3.0.1", - "ajv-keywords": "^5.1.0", - "jsonpointer": "^5.0.0", - "lodash": "^4.17.21", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/supertest": "^6.0.2", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/doorkeeper", + "version": "0.0.1", + "description": "MMS - API - Doorkeeper", + "keywords": [ + "NodeJS", + "MMS", + "API", + "AJV" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/doorkeeper" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "doc": "typedoc --options typedoc.json", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/doorkeeper/licenses.csv --customPath ../../../.config/customFormat.json", + "lint": "eslint \"src/**/*.ts\" --quiet --fix", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/crash": "*", + "ajv": "^8.17.1", + "ajv-errors": "^3.0.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0", + "jsonpointer": "^5.0.0", + "lodash": "^4.17.21", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13", + "@types/supertest": "^6.0.2" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/doorkeeper/src/Doorkeeper.ts b/packages/api/doorkeeper/src/Doorkeeper.ts index 70b067f4..fa0c7759 100644 --- a/packages/api/doorkeeper/src/Doorkeeper.ts +++ b/packages/api/doorkeeper/src/Doorkeeper.ts @@ -1,441 +1,439 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Crash, Multi } from '@mdf.js/crash'; -import AJV, { AnySchema, ErrorObject, Options, SchemaObject } from 'ajv'; -import AJVError from 'ajv-errors'; -import AJVFormats from 'ajv-formats'; -import AJVKeyWords from 'ajv-keywords'; -import { AnyValidateFunction } from 'ajv/dist/core'; -import { get } from 'jsonpointer'; -import { cloneDeep, forOwn, omit } from 'lodash'; -import { v4 } from 'uuid'; - -import DynamicDefaults, { DynamicDefaultFunc } from 'ajv-keywords/dist/definitions/dynamicDefaults'; - -const DEFAULT_SNIPPET_META_SCHEMA = { - title: 'Default snippets', - type: 'array', - items: { - title: 'VSCode snippet', - type: 'object', - properties: { - label: { type: 'string' }, - description: { type: 'string' }, - body: {}, - }, - required: ['label', 'body'], - }, -}; - -export type { JSONSchemaType, SchemaObject } from 'ajv'; -export type SchemaSelector = T extends void ? string : keyof T & string; -export type ValidatedOutput = K extends keyof T ? T[K] : any; - -/** - * This is the AJV Options object, but `allErrors` property is always true by default - * - * See [AJV Options](https://ajv.js.org/options.html) for more information - */ -export interface DoorkeeperOptions extends Omit { - /** Dynamic defaults to be used in the schemas */ - dynamicDefaults?: Record; -} - -/** Callback function for the validation process */ -export type ResultCallback = (error?: Crash | Multi, result?: ValidatedOutput) => void; - -/** - * Doorkeeper is a wrapper for AJV that allows us to validate JSONs against schemas. - * It also allows us to register schemas and retrieve them later. - * @category Doorkeeper - * @public - */ -export class DoorKeeper { - readonly uuid = v4(); - /** AJV instance*/ - private readonly ajv: AJV; - /** - * Creates the Doorkeeper instance to validate JSONs using AJV with formats, keywords and errors - * @param options - Doorkeeper options - */ - constructor(public readonly options?: DoorkeeperOptions) { - const AJVOptions: Options = this.options - ? { ...this.options, allErrors: true } - : { allErrors: true }; - if (this.options?.dynamicDefaults) { - for (const [key, func] of Object.entries(this.options.dynamicDefaults)) { - DynamicDefaults.DEFAULTS[key] = func; - } - } - this.ajv = AJVFormats(AJVKeyWords(AJVError(new AJV(AJVOptions)))); - this.ajv.addKeyword({ keyword: 'markdownDescription', schemaType: 'string', valid: true }); - this.ajv.addKeyword({ - keyword: 'defaultSnippets', - metaSchema: DEFAULT_SNIPPET_META_SCHEMA, - valid: true, - }); - this.options = AJVOptions; - } - /** - * Add a new schema to the ajv collection - * @param schema - schema to be added - * @param key - identification key for the schema - */ - private addSchema(schema: SchemaObject | AnySchema, key: string): void { - try { - this.ajv.addSchema(schema, key); - } catch (rawError) { - const error = Crash.from(rawError); - throw new Crash(`Error adding the schema: [${key}] - error: [${error.message}]`, this.uuid, { - cause: error, - }); - } - } - /** - * Compiles the schemas, adding them to the ajv collection - * @param schemas - schemas to be compiled - */ - private compileSchemas(schemas: AnySchema[]): void { - for (const schema of schemas) { - try { - this.ajv.compile(schema); - } catch (rawError) { - const error = Crash.from(rawError); - throw new Crash( - `Error adding the schema: [${JSON.stringify(schema)}] - error: [${error.message}]`, - this.uuid, - { cause: error } - ); - } - } - } - /** - * Create a Multi error from AJV ErrorObject array - * @param errors - AJV errors - * @param schema - schema applied - * @param uuid - uuid string - * @param data - JSON to be validated - * @param stackableError - Error where the new crash errors should be stacked - */ - private multify( - errors: ErrorObject[], - schema: string, - uuid: string, - data: any, - stackableError?: Multi - ): Multi { - const validationError = stackableError - ? stackableError - : new Multi(`Errors during the schema validation process`, uuid, { - name: 'ValidationError', - info: { data, schema }, - }); - for (const error of errors) { - if ('emUsed' in error && error.emUsed) { - continue; - } - const message = this.errorFormatter(error, data); - validationError.push(new Crash(message, uuid, { name: 'ValidationError', info: error })); - if (error.params && error.params['errors']) { - this.multify(error.params['errors'], schema, uuid, data, validationError); - } - } - return validationError; - } - /** - * Create a human readable error message - * @param error - AJV error - * @param data - JSON to be validated - */ - private errorFormatter(error: ErrorObject, data: any): string { - let message = `${error.message}`; - if (error.propertyName) { - message += ` - Property: [${error.propertyName}]`; - } - if (error.keyword === 'additionalProperties') { - message += ` - Property: [${ - //@ts-ignore additionalProperties exists if the keyword is additionalProperties - error.params.additionalProperties || error.params.additionalProperty - }]`; - } - let value; - if (error.instancePath) { - message += ` - Path: [${error.instancePath}]`; - value = get(data, error.instancePath); - } else { - value = data; - } - switch (typeof value) { - case 'undefined': - message += ` - no value`; - break; - case 'symbol': - message += ` - Value: [${value.toString()}]`; - break; - case 'object': - message += ` - Value: [${JSON.stringify(value)}]`; - break; - default: - message += ` - Value: [${value}]`; - } - return message; - } - /** - * Get a schema from the ajv collection - * @param schema - schema to be retrieved - * @param uuid - uuid string - * @returns the schema validator - * @throws Crash if the schema is not registered - */ - private getSchema>( - schema: K, - uuid: string - ): AnyValidateFunction { - const validator = this.ajv.getSchema(schema); - if (validator === undefined) { - throw new Crash(`${schema} is not registered in the collection.`, uuid, { - name: 'ValidationError', - info: { schema }, - }); - } - return validator; - } - /** - * Validates a JSON against a schema - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param uuid - unique identifier for this operation - */ - private checkSchema>(schema: K, data: any, uuid = v4()): void { - const validator = this.getSchema(schema, uuid); - if (!validator(data)) { - if (validator.errors) { - throw this.multify(validator.errors, schema, uuid, data); - } else { - throw new Crash(`Unexpected error in JSON schema validation process`, uuid, { - name: 'ValidationError', - info: { schema, data }, - }); - } - } - } - /** - * Registers a group of schemas from an object using the keys of the - * object as key and the value as the validation schema - * @param schemas - Object containing the [key, validation schema] - * @returns - the instance - */ - public register(schemas: Record, AnySchema>): DoorKeeper; - /** - * Registers a group of schemas from an array and compiles them - * @param schemas - Array containing the - * @returns - the instance - */ - public register(schemas: AnySchema[]): DoorKeeper; - /** - * Registers one schema with its key - * @param key - the key with which identify the schema - * @param validatorSchema - the schema to be registered - * @returns - the instance - */ - public register(key: SchemaSelector, validatorSchema: AnySchema): DoorKeeper; - public register( - keyOrArraySchemasOrObjectSchemas: SchemaSelector | AnySchema[] | Record, - validatorSchema?: AnySchema - ): DoorKeeper { - if ( - typeof keyOrArraySchemasOrObjectSchemas === 'string' && - typeof validatorSchema === 'object' && - !Array.isArray(validatorSchema) - ) { - this.addSchema(validatorSchema, keyOrArraySchemasOrObjectSchemas); - } else if (Array.isArray(keyOrArraySchemasOrObjectSchemas)) { - this.compileSchemas(keyOrArraySchemasOrObjectSchemas); - } else if (typeof keyOrArraySchemasOrObjectSchemas === 'object') { - for (const [key, _schema] of Object.entries(keyOrArraySchemasOrObjectSchemas)) { - this.addSchema(_schema, key); - } - } else { - throw new Crash('Invalid parameters, no schema will be registered', this.uuid); - } - return this; - } - /** - * Checks if the input schema is registered - * @param schema - schema asked for - * @returns - if the schema is registered in the ajv collection - */ - public isSchemaRegistered>(schema: K): boolean { - return !!this.ajv.getSchema(schema); - } - /** - * Validate an Object against the input schema - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param uuid - unique identifier for this operation - * @param callback - callback function with the result of the validation - */ - public validate>( - schema: K, - data: any, - uuid: string, - callback: ResultCallback - ): void; - /** - * Validate an Object against the input schema - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param callback - callback function with the result of the validation - */ - public validate>( - schema: K, - data: any, - callback: ResultCallback - ): void; - /** - * Validate an Object against the input schema - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param uuid - unique identifier for this operation - */ - public validate>( - schema: K, - data: any, - uuid: string - ): Promise>; - /** - * Validate an Object against the input schema - * @param schema - The schema we want to validate - * @param data - Object to be validated - */ - public validate>( - schema: K, - data: any - ): Promise>; - public validate>( - schema: K, - data: any, - uuidOrCallBack?: string | ResultCallback, - callbackOrUndefined?: ResultCallback - ): Promise> | void { - let error: Crash | Multi | undefined; - const uuid = typeof uuidOrCallBack === 'string' ? uuidOrCallBack : v4(); - const callback = typeof uuidOrCallBack === 'function' ? uuidOrCallBack : callbackOrUndefined; - try { - this.checkSchema(schema, data, uuid); - } catch (rawError) { - error = Crash.from(rawError); - } - if (callback) { - callback(error, data); - } else { - if (error) { - return Promise.reject(error); - } else { - return Promise.resolve(data); - } - } - } - /** - * Try to validate an Object against the input schema or throw a ValidationError - * @param schema - The schema we want to validate - * @param data - Object to be validated - */ - public attempt>(schema: K, data: any): ValidatedOutput; - /** - * Try to validate an Object against the input schema or throw a ValidationError - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param uuid - unique identifier for this operation - */ - public attempt>( - schema: K, - data: any, - uuid: string - ): ValidatedOutput; - public attempt>( - schema: K, - data: any, - uuid?: string - ): ValidatedOutput { - this.checkSchema(schema, data, uuid); - return data as ValidatedOutput; - } - /** - * Validate an Object against the input schema and return a boolean - * @param schema - The schema we want to validate - * @param data - Object to be validated - */ - public check>(schema: K, data: any): boolean; - /** - * Validate an Object against the input schema and return a boolean - * @param schema - The schema we want to validate - * @param data - Object to be validated - * @param uuid - unique identifier for this operation - */ - public check>(schema: K, data: any, uuid: string): boolean; - public check>(schema: K, data: any, uuid?: string): boolean { - try { - this.checkSchema(schema, data, uuid); - return true; - } catch (error) { - return false; - } - } - /** - * Checks if the given data matches the specified schema. - * @param schema - The schema to check against. - * @param data - The data to validate. - * @returns A boolean indicating whether the data matches the schema. - */ - public is>(schema: K, data: any): data is ValidatedOutput { - return this.check(schema, data); - } - /** - * Return a dereferenced schema with all the $ref resolved - * @param schema - The schema we want to dereference - * @param uuid - unique identifier for this operation - * @returns A dereferenced schema with all the $ref resolved - * @experimental This method is experimental and might change in the future without notice or be - * removed from a future release. Use it at your own risk. - */ - public dereference>(schema: K, uuid = v4()): AnySchema { - const validatorSchema = this.getSchema(schema, uuid); - if (typeof validatorSchema.schema === 'boolean') { - throw new Crash('Invalid schema, no schema will be dereferenced', uuid, { - name: 'ValidationError', - info: { schema }, - }); - } - const _schema = cloneDeep(validatorSchema.schema); - const iterator = ( - propertyValue: Record | string, - propertyKey: string, - parentObject: Record - ) => { - if (typeof propertyValue === 'string' && propertyKey === '$ref') { - const refSchema = this.getSchema(propertyValue as SchemaSelector, uuid); - const copyOfRefSchema = omit(cloneDeep(refSchema.schema) as object, ['$id', '$schema']); - const dereferencedSchema = forOwn(copyOfRefSchema, iterator); - Object.assign(parentObject, omit(dereferencedSchema, ['$ref'])); - } else if (typeof propertyValue === 'object' && propertyValue['$ref']) { - const refSchema = this.getSchema(propertyValue['$ref'], uuid); - const copyOfRefSchema = omit(cloneDeep(refSchema.schema) as object, ['$id', '$schema']); - const copyOfEntry = omit(cloneDeep(propertyValue), ['$ref']); - const dereferencedSchema = { - ...forOwn(copyOfRefSchema, iterator), - ...copyOfEntry, - }; - parentObject[propertyKey] = omit(dereferencedSchema, ['$ref']); - } else if (typeof propertyValue === 'object') { - parentObject[propertyKey] = forOwn(propertyValue, iterator); - } - }; - return forOwn(_schema, iterator); - } -} +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Crash, Multi } from '@mdf.js/crash'; +import AJV, { AnySchema, ErrorObject, Options, SchemaObject } from 'ajv'; +import AJVError from 'ajv-errors'; +import AJVFormats from 'ajv-formats'; +import AJVKeyWords from 'ajv-keywords'; +import { AnyValidateFunction } from 'ajv/dist/core'; +import { get } from 'jsonpointer'; +import { cloneDeep, forOwn, omit } from 'lodash'; +import { v4 } from 'uuid'; + +import DynamicDefaults, { DynamicDefaultFunc } from 'ajv-keywords/dist/definitions/dynamicDefaults'; + +const DEFAULT_SNIPPET_META_SCHEMA = { + title: 'Default snippets', + type: 'array', + items: { + title: 'VSCode snippet', + type: 'object', + properties: { + label: { type: 'string' }, + description: { type: 'string' }, + body: {}, + }, + required: ['label', 'body'], + }, +}; + +export type { JSONSchemaType, SchemaObject } from 'ajv'; +export type SchemaSelector = T extends void ? string : keyof T & string; +export type ValidatedOutput = K extends keyof T ? T[K] : any; + +/** + * This is the AJV Options object, but `allErrors` property is always true by default + * + * See [AJV Options](https://ajv.js.org/options.html) for more information + */ +export interface DoorkeeperOptions extends Omit { + /** Dynamic defaults to be used in the schemas */ + dynamicDefaults?: Record; +} + +/** Callback function for the validation process */ +export type ResultCallback = (error?: Crash | Multi, result?: ValidatedOutput) => void; + +/** + * Doorkeeper is a wrapper for AJV that allows us to validate JSONs against schemas. + * It also allows us to register schemas and retrieve them later. + * @category Doorkeeper + * @public + */ +export class DoorKeeper { + readonly uuid = v4(); + /** AJV instance*/ + private readonly ajv: AJV; + /** + * Creates the Doorkeeper instance to validate JSONs using AJV with formats, keywords and errors + * @param options - Doorkeeper options + */ + constructor(public readonly options?: DoorkeeperOptions) { + const AJVOptions: Options = this.options + ? { ...this.options, allErrors: true } + : { allErrors: true }; + if (this.options?.dynamicDefaults) { + for (const [key, func] of Object.entries(this.options.dynamicDefaults)) { + DynamicDefaults.DEFAULTS[key] = func; + } + } + this.ajv = AJVFormats(AJVKeyWords(AJVError(new AJV(AJVOptions)))); + this.ajv.addKeyword({ keyword: 'markdownDescription', schemaType: 'string', valid: true }); + this.ajv.addKeyword({ + keyword: 'defaultSnippets', + metaSchema: DEFAULT_SNIPPET_META_SCHEMA, + valid: true, + }); + this.options = AJVOptions; + } + /** + * Add a new schema to the ajv collection + * @param schema - schema to be added + * @param key - identification key for the schema + */ + private addSchema(schema: SchemaObject | AnySchema, key: string): void { + try { + this.ajv.addSchema(schema, key); + } catch (rawError) { + const error = Crash.from(rawError); + throw new Crash(`Error adding the schema: [${key}] - error: [${error.message}]`, this.uuid, { + cause: error, + }); + } + } + /** + * Compiles the schemas, adding them to the ajv collection + * @param schemas - schemas to be compiled + */ + private compileSchemas(schemas: AnySchema[]): void { + for (const schema of schemas) { + try { + this.ajv.compile(schema); + } catch (rawError) { + const error = Crash.from(rawError); + throw new Crash( + `Error adding the schema: [${JSON.stringify(schema)}] - error: [${error.message}]`, + this.uuid, + { cause: error } + ); + } + } + } + /** + * Create a Multi error from AJV ErrorObject array + * @param errors - AJV errors + * @param schema - schema applied + * @param uuid - uuid string + * @param data - JSON to be validated + * @param stackableError - Error where the new crash errors should be stacked + */ + private multify( + errors: ErrorObject[], + schema: string, + uuid: string, + data: any, + stackableError?: Multi + ): Multi { + const validationError = + stackableError ?? + new Multi(`Errors during the schema validation process`, uuid, { + name: 'ValidationError', + info: { data, schema }, + }); + for (const error of errors) { + if ('emUsed' in error && error.emUsed) { + continue; + } + const message = this.errorFormatter(error, data); + validationError.push(new Crash(message, uuid, { name: 'ValidationError', info: error })); + if (error.params?.['errors']) { + this.multify(error.params['errors'], schema, uuid, data, validationError); + } + } + return validationError; + } + /** + * Create a human readable error message + * @param error - AJV error + * @param data - JSON to be validated + */ + private errorFormatter(error: ErrorObject, data: any): string { + let message = `${error.message}`; + if (error.propertyName) { + message += ` - Property: [${error.propertyName}]`; + } + if (error.keyword === 'additionalProperties') { + message += ` - Property: [${ + //@ts-ignore additionalProperties exists if the keyword is additionalProperties + error.params.additionalProperties || error.params.additionalProperty + }]`; + } + let value; + if (error.instancePath) { + message += ` - Path: [${error.instancePath}]`; + value = get(data, error.instancePath); + } else { + value = data; + } + switch (typeof value) { + case 'undefined': + message += ` - no value`; + break; + case 'symbol': + message += ` - Value: [${value.toString()}]`; + break; + case 'object': + message += ` - Value: [${JSON.stringify(value)}]`; + break; + default: + message += ` - Value: [${value}]`; + } + return message; + } + /** + * Get a schema from the ajv collection + * @param schema - schema to be retrieved + * @param uuid - uuid string + * @returns the schema validator + * @throws Crash if the schema is not registered + */ + private getSchema>( + schema: K, + uuid: string + ): AnyValidateFunction { + const validator = this.ajv.getSchema(schema); + if (validator === undefined) { + throw new Crash(`${schema} is not registered in the collection.`, uuid, { + name: 'ValidationError', + info: { schema }, + }); + } + return validator; + } + /** + * Validates a JSON against a schema + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param uuid - unique identifier for this operation + */ + private checkSchema>(schema: K, data: any, uuid = v4()): void { + const validator = this.getSchema(schema, uuid); + if (!validator(data)) { + if (validator.errors) { + throw this.multify(validator.errors, schema, uuid, data); + } else { + throw new Crash(`Unexpected error in JSON schema validation process`, uuid, { + name: 'ValidationError', + info: { schema, data }, + }); + } + } + } + /** + * Registers a group of schemas from an object using the keys of the + * object as key and the value as the validation schema + * @param schemas - Object containing the [key, validation schema] + * @returns - the instance + */ + public register(schemas: Record, AnySchema>): DoorKeeper; + /** + * Registers a group of schemas from an array and compiles them + * @param schemas - Array containing the + * @returns - the instance + */ + public register(schemas: AnySchema[]): DoorKeeper; + /** + * Registers one schema with its key + * @param key - the key with which identify the schema + * @param validatorSchema - the schema to be registered + * @returns - the instance + */ + public register(key: SchemaSelector, validatorSchema: AnySchema): DoorKeeper; + public register( + keyOrArraySchemasOrObjectSchemas: SchemaSelector | AnySchema[] | Record, + validatorSchema?: AnySchema + ): DoorKeeper { + if ( + typeof keyOrArraySchemasOrObjectSchemas === 'string' && + typeof validatorSchema === 'object' && + !Array.isArray(validatorSchema) + ) { + this.addSchema(validatorSchema, keyOrArraySchemasOrObjectSchemas); + } else if (Array.isArray(keyOrArraySchemasOrObjectSchemas)) { + this.compileSchemas(keyOrArraySchemasOrObjectSchemas); + } else if (typeof keyOrArraySchemasOrObjectSchemas === 'object') { + for (const [key, _schema] of Object.entries(keyOrArraySchemasOrObjectSchemas)) { + this.addSchema(_schema, key); + } + } else { + throw new Crash('Invalid parameters, no schema will be registered', this.uuid); + } + return this; + } + /** + * Checks if the input schema is registered + * @param schema - schema asked for + * @returns - if the schema is registered in the ajv collection + */ + public isSchemaRegistered>(schema: K): boolean { + return !!this.ajv.getSchema(schema); + } + /** + * Validate an Object against the input schema + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param uuid - unique identifier for this operation + * @param callback - callback function with the result of the validation + */ + public validate>( + schema: K, + data: any, + uuid: string, + callback: ResultCallback + ): void; + /** + * Validate an Object against the input schema + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param callback - callback function with the result of the validation + */ + public validate>( + schema: K, + data: any, + callback: ResultCallback + ): void; + /** + * Validate an Object against the input schema + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param uuid - unique identifier for this operation + */ + public validate>( + schema: K, + data: any, + uuid: string + ): Promise>; + /** + * Validate an Object against the input schema + * @param schema - The schema we want to validate + * @param data - Object to be validated + */ + public validate>( + schema: K, + data: any + ): Promise>; + public validate>( + schema: K, + data: any, + uuidOrCallBack?: string | ResultCallback, + callbackOrUndefined?: ResultCallback + ): Promise> | void { + let error: Crash | Multi | undefined; + const uuid = typeof uuidOrCallBack === 'string' ? uuidOrCallBack : v4(); + const callback = typeof uuidOrCallBack === 'function' ? uuidOrCallBack : callbackOrUndefined; + try { + this.checkSchema(schema, data, uuid); + } catch (rawError) { + error = Crash.from(rawError); + } + if (callback) { + callback(error, data); + } else if (error) { + return Promise.reject(error); + } else { + return Promise.resolve(data); + } + } + /** + * Try to validate an Object against the input schema or throw a ValidationError + * @param schema - The schema we want to validate + * @param data - Object to be validated + */ + public attempt>(schema: K, data: any): ValidatedOutput; + /** + * Try to validate an Object against the input schema or throw a ValidationError + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param uuid - unique identifier for this operation + */ + public attempt>( + schema: K, + data: any, + uuid: string + ): ValidatedOutput; + public attempt>( + schema: K, + data: any, + uuid?: string + ): ValidatedOutput { + this.checkSchema(schema, data, uuid); + return data as ValidatedOutput; + } + /** + * Validate an Object against the input schema and return a boolean + * @param schema - The schema we want to validate + * @param data - Object to be validated + */ + public check>(schema: K, data: any): boolean; + /** + * Validate an Object against the input schema and return a boolean + * @param schema - The schema we want to validate + * @param data - Object to be validated + * @param uuid - unique identifier for this operation + */ + public check>(schema: K, data: any, uuid: string): boolean; + public check>(schema: K, data: any, uuid?: string): boolean { + try { + this.checkSchema(schema, data, uuid); + return true; + } catch (error) { + return false; + } + } + /** + * Checks if the given data matches the specified schema. + * @param schema - The schema to check against. + * @param data - The data to validate. + * @returns A boolean indicating whether the data matches the schema. + */ + public is>(schema: K, data: any): data is ValidatedOutput { + return this.check(schema, data); + } + /** + * Return a dereferenced schema with all the $ref resolved + * @param schema - The schema we want to dereference + * @param uuid - unique identifier for this operation + * @returns A dereferenced schema with all the $ref resolved + * @experimental This method is experimental and might change in the future without notice or be + * removed from a future release. Use it at your own risk. + */ + public dereference>(schema: K, uuid = v4()): AnySchema { + const validatorSchema = this.getSchema(schema, uuid); + if (typeof validatorSchema.schema === 'boolean') { + throw new Crash('Invalid schema, no schema will be dereferenced', uuid, { + name: 'ValidationError', + info: { schema }, + }); + } + const _schema = cloneDeep(validatorSchema.schema); + const iterator = ( + propertyValue: Record | string, + propertyKey: string, + parentObject: Record + ) => { + if (typeof propertyValue === 'string' && propertyKey === '$ref') { + const refSchema = this.getSchema(propertyValue as SchemaSelector, uuid); + const copyOfRefSchema = omit(cloneDeep(refSchema.schema) as object, ['$id', '$schema']); + const dereferencedSchema = forOwn(copyOfRefSchema, iterator); + Object.assign(parentObject, omit(dereferencedSchema, ['$ref'])); + } else if (typeof propertyValue === 'object' && propertyValue['$ref']) { + const refSchema = this.getSchema(propertyValue['$ref'], uuid); + const copyOfRefSchema = omit(cloneDeep(refSchema.schema) as object, ['$id', '$schema']); + const copyOfEntry = omit(cloneDeep(propertyValue), ['$ref']); + const dereferencedSchema = { + ...forOwn(copyOfRefSchema, iterator), + ...copyOfEntry, + }; + parentObject[propertyKey] = omit(dereferencedSchema, ['$ref']); + } else if (typeof propertyValue === 'object') { + parentObject[propertyKey] = forOwn(propertyValue, iterator); + } + }; + return forOwn(_schema, iterator); + } +} diff --git a/packages/api/faker/README.md b/packages/api/faker/README.md index 4923bfec..97ae8ab5 100644 --- a/packages/api/faker/README.md +++ b/packages/api/faker/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/faker/package.json b/packages/api/faker/package.json index bb69c807..33b5da44 100644 --- a/packages/api/faker/package.json +++ b/packages/api/faker/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/chance": "^1.1.3", - "@types/lodash": "^4.17.10" + "@types/lodash": "^4.17.13" }, "engines": { "node": ">=16.14.2" diff --git a/packages/api/faker/src/Factory.ts b/packages/api/faker/src/Factory.ts index fd87ca7d..c65d4e76 100644 --- a/packages/api/faker/src/Factory.ts +++ b/packages/api/faker/src/Factory.ts @@ -1,594 +1,593 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Crash } from '@mdf.js/crash'; -import { Chance } from 'chance'; -import _, { merge } from 'lodash'; - -/** Type for attribute dependencies */ -export type Dependencies = (K | string)[]; -/** Type for function for attribute builder function */ -export type Builder = (...args: any) => T[K] | undefined; -/** Type for attribute default value */ -export type DefaultValue = T[K]; -/** Type for attribute value option */ -type GeneratorOptions = - | Builder - | DefaultValue - | Dependencies; -/** Interface for attribute generation */ -interface Entry { - dependencies?: Dependencies; - builder: Builder; -} -/** Interface for default object */ -export interface DefaultObject { - [key: string]: any; -} -/** Interface for default options */ -export interface DefaultOptions { - likelihood?: number; - [key: string]: any; -} - -/** Factory for building JavaScript objects, mostly useful for setting up test data */ -export class Factory< - T extends DefaultObject = DefaultObject, - R extends DefaultOptions = DefaultOptions, -> { - /** Options for programmatic generation of attributes */ - private _opts: { - [K in keyof R]: Entry; - }; - /** Attributes of this factory, based on a interface */ - private _attrs: { - [K in keyof T]: Entry; - }; - /** Auto incrementing sequence attribute */ - private _seques: { - [K in keyof T]?: number; - }; - /** Callback function array */ - private readonly _callbacks: ((object: T, options: R) => T)[]; - /** Chance object for probabilistic wrong value generation */ - private readonly _chance; - /** Create a new factory instance */ - public constructor() { - this._opts = {} as { [K in keyof R]: Entry }; - this._attrs = {} as { [K in keyof T]: Entry }; - this._seques = {}; - this._callbacks = []; - this._chance = new Chance(); - } - /** - * Define an attribute on this factory - * @param attr - Name of attribute - * @example - * ```typescript - * factory.attr('name'); - * ``` - */ - public attr(attr: K): Factory; - /** - * Define an attribute on this factory using a default value (e.g. a string or number) - * @param attr - Name of attribute - * @param defaultValue - Default value of attribute - * @example - * ```typescript - * factory.attr('name', 'John Doe'); - * ``` - */ - public attr(attr: K, defaultValue: DefaultValue): Factory; - /** - * Define an attribute on this factory using a generator function - * @param attr - Name of attribute - * @param generator - Value generator function - * @example - * ```typescript - * factory.attr('name', () => function() { return 'John Doe'; }); - * ``` - */ - public attr(attr: K, generator: Builder): Factory; - /** - * Define an attribute on this factory using a generator function and dependencies on options or - * other attributes - * @param attr - Name of attribute - * @param dependencies - Array of dependencies as option or attribute names that are used by the - * generator function to generate the value of this attribute - * @param generator - Value generator function. The generator function will be called with the - * resolved values of the dependencies as arguments. - * @example - * ```typescript - * factory.attr('name', ['firstName', 'lastName'], (firstName, lastName) => { - * return `${firstName} ${lastName}`; - * }); - * ``` - */ - public attr( - attr: K, - dependencies: Dependencies, - generator: Builder - ): Factory; - public attr( - attr: K, - generatorOptions?: GeneratorOptions, - builder?: Builder - ): Factory { - this._attrs[attr] = this._SafeType(generatorOptions, builder); - return this; - } - /** - * Define multiple attributes on this factory using a default value (e.g. a string or number) or - * generator function. If you need to define dependencies on options or other attributes, use the - * `attr` method instead. - * @param attributes - Object with multiple attributes - * @example - * ```typescript - * factory.attrs({ - * name: 'John Doe', - * age: function() { return 21; }, - * }); - * ``` - */ - public attrs(attributes: { - [K in keyof T]: DefaultValue | Builder; - }): Factory { - for (const attr in attributes) { - if (attributes.hasOwnProperty(attr)) { - this.attr(attr, attributes[attr]); - } - } - return this; - } - /** - * Define an option for this factory using a default value. Options are values that are not - * directly used in the generated object, but can be used to influence the generation process. - * For example, you could define an option `withAddress` that, when set to `true`, would generate - * an address and add it to the generated object. Like attributes, options can have dependencies - * on other options but not on attributes. - * @param opt - Name of option - * @param defaultValue - Default value of option - * @example - * ```typescript - * factory.option('withAddress', false); - * ``` - */ - public option(opt: K, defaultValue: DefaultValue): Factory; - /** - * Define an option for this factory using a generator function. Options are values that are not - * directly used in the generated object, but can be used to influence the generation process. - * For example, you could define an option `withAddress` that, when set to `true`, would generate - * an address and add it to the generated object. Like attributes, options can have dependencies - * on other options but not on attributes. - * @param opt - Name of option - * @param generator - Value generator function - * @example - * ```typescript - * factory.option('withAddress', () => function() { return false; }); - * ``` - */ - public option(opt: K, generator: Builder): Factory; - /** - * Define an option for this factory using a generator function with dependencies in other - * options. Options are values that are not directly used in the generated object, but can be - * used to influence the generation process. For example, you could define an option - * `withAddress` that, when set to `true`, would generate an address and add it to the generated - * object. Like attributes, options can have dependencies on other options but not on attributes. - * @param opt - Name of option - * @param dependencies - Array of dependencies as option names that are used by the generator - * function to generate the value of this option - * @param generator - Value generator function with dependencies in other options. The generator - * function will be called with the resolved values of the dependencies as arguments. - */ - public option( - opt: K, - dependencies: Dependencies, - generator: Builder - ): Factory; - public option( - opt: K, - generatorOptions: GeneratorOptions, - builder?: Builder - ): Factory { - this._opts[opt] = this._SafeType(generatorOptions, builder); - return this; - } - /** - * Define an auto incrementing sequence attribute of the object. Default value is 1. - * @param attr - Name of attribute - * @example - * ```typescript - * factory.sequence('id'); - * ``` - */ - public sequence(attr: K): Factory; - /** - * Define an auto incrementing sequence attribute of the object where the sequence value is - * generated by a generator function that is called with the current sequence value as argument. - * @param attr - Name of attribute - * @param generator - Value generator function - * @example - * ```typescript - * factory.sequence('id', (i) => function() { return i + 11; }); - * ``` - */ - public sequence(attr: K, generator: Builder): Factory; - /** - * Define an auto incrementing sequence attribute of the object where the sequence value is - * generated by a generator function that is called with the current sequence value as argument - * and dependencies on options or other attributes. - * @param attr - Name of attribute - * @param dependencies - Array of dependencies as option or attribute names that are used by the - * generator function to generate the value of the sequence attribute - * @param generator - Value generator function - * @example - * ```typescript - * factory.sequence('id', ['idPrefix'], (i, idPrefix) => function() { - * return `${idPrefix}${i}`; - * }); - * ``` - */ - public sequence( - attr: K, - dependencies: (K | string)[], - generator: Builder - ): Factory; - public sequence( - attr: K, - generatorOptions?: GeneratorOptions, - builder?: Builder - ): Factory { - const _attribute = this._SafeType(generatorOptions, builder); - if (generatorOptions === undefined && builder === undefined) { - _attribute.builder = i => i + 1; - _attribute.dependencies = []; - } - - return this.attr(attr, _attribute.dependencies || [], (...args: any[]) => { - const value = _attribute.builder(this._seques[attr] || 0, ...args); - this._seques[attr] = value; - return value; - }); - } - /** - * Register a callback function to be called after the object is generated. The callback function - * receives the generated object as first argument and the resolved options as second argument. - * @param callback - Callback function - * @example - * ```typescript - * factory.after((user) => { - * user.name = user.name.toUpperCase(); - * }); - * ``` - */ - public after(callback: (object: T, options: R) => T): Factory { - this._callbacks.push(callback); - return this; - } - /** - * Returns an object that is generated by the factory. - * The optional option `likelihood` is a number between 0 and 100 that defines the probability - * that the generated object contains wrong data. This is useful for testing if your code can - * handle wrong data. The default value is 100, which means that the generated object always - * contains correct data. - * @param attributes - object containing attribute override key value pairs - * @param options - object containing option key value pairs - */ - public build( - attributes: { [K in keyof T]?: T[K] } = {}, - options: { likelihood?: number; [key: string]: any } = { likelihood: 100 } - ): T { - if (options && options['likelihood'] === undefined) { - options = { ...options, likelihood: 100 }; - } - if ( - typeof options['likelihood'] !== 'number' || - options['likelihood'] < 0 || - options['likelihood'] > 100 - ) { - throw new Crash('Likelihood must be a number between 0 and 100', { - likelihood: options['likelihood'], - }); - } - const _attributes = _.merge(_.cloneDeep(this._attrs), this._convertToAttributes(attributes)); - const _options = _.merge(this._opts, this._ConvertToOptions(options)); - let returnableObject: { [K in keyof T]?: T[K] } = {}; - const resolvedOptions: { [key: string]: any } = {}; - for (const attr in this._attrs) { - const stack: (keyof T | string)[] = []; - returnableObject[attr] = this._Build( - _attributes[attr] as Entry, - returnableObject, - resolvedOptions, - _attributes, - _options, - stack, - options['likelihood'] - ); - } - if ( - (Object.keys(this._attrs).length === 0 || Object.keys(resolvedOptions).length === 0) && - Object.keys(_options).length > 0 - ) { - for (const opts in _options) { - const stack: (keyof T | string)[] = []; - resolvedOptions[opts] = this._Build( - _options[opts], - returnableObject, - resolvedOptions, - _attributes, - _options, - stack - ); - } - } - for (const callback of this._callbacks) { - const obj = callback(returnableObject as T, resolvedOptions as R); - if (obj !== undefined) { - returnableObject = obj; - } - } - return returnableObject as T; - } - /** - * Returns an array of objects that are generated by the factory. - * The optional option `likelihood` is a number between 0 and 100 that defines the probability - * that the generated object contains wrong data. This is useful for testing if your code can - * handle wrong data. The default value is 100, which means that the generated object always - * contains correct data. - * @param size - number of objects to generate - * @param attributes - object containing attribute override key value pairs - * @param options - object containing option key value pairs - * @example - * ```typescript - * factory.buildList(3, { name: 'John Doe' }); - * ``` - */ - public buildList( - size: number, - attributes: { [K in keyof T]?: T[K] } = {}, - options: { likelihood?: number; [key: string]: any } = { likelihood: 100 } - ): T[] { - const objs = []; - for (let i = 0; i < size; i++) { - objs.push(this.build(attributes, options)); - } - return objs; - } - /** - * Extend this factory with another factory. The attributes and options of the other factory are - * merged into this factory. If an attribute or option with the same name already exists, it is - * overwritten. - * @param factory - Factory to extend this factory with - */ - public extend

                          , J extends Partial>(factory: Factory): Factory { - Object.assign(this._attrs, factory._attrs); - Object.assign(this._opts, factory._opts); - this._callbacks.push(...factory._callbacks.map(this.wrapCallback)); - return this; - } - /** Reset all the sequences of this factory */ - public reset(): void { - this._seques = {}; - } - /** - * Wrap a callback function to add type safety and avoid lost of data of extended factories - * @param callback - Callback function - */ - private readonly wrapCallback =

                          , J extends Partial>( - callback: (object: P, options: J) => P - ): ((object: T, options: R) => T) => { - return (object: T, options: R) => { - const result = callback(object as unknown as P, options as unknown as J); - return merge(object, result) as T; - }; - }; - /** - * Create an object with standard Options from a key-value pairs object - * @param options - object containing option key value pairs - */ - private _ConvertToOptions(options: { [key: string]: any }): { [key: string]: Entry } { - const opts: { [key: string]: Entry } = {}; - for (const opt in options) { - opts[opt] = { dependencies: undefined, builder: () => options[opt] }; - } - return opts; - } - /** - * Resolve the value of the options or attributes - * @param meta - metadata information of option or attribute - * @param object - object with resolved attributes - * @param resolvedOptions - object with resolved options - * @param attributes - attributes object - * @param options - options object - * @param stack - stack of recursive calls - */ - private _Build( - meta: Entry, - object: { [K in keyof T]?: T[K] }, - resolvedOptions: { [key: string]: any }, - attributes: { [K in keyof T]?: Entry }, - options: { [key: string]: Entry }, - stack: (keyof T | string)[], - likelihood = 100 - ): any { - if (!meta) { - throw new Crash('Error in factory build process', { - meta, - object, - resolvedOptions, - attributes, - options, - stack, - }); - } - if (!meta.dependencies) { - if (!this._chance.bool({ likelihood })) { - return this._wrongData(typeof meta.builder()); - } else { - return meta.builder(); - } - } else { - const args = this._BuildWithDependencies( - meta.dependencies, - object, - resolvedOptions, - attributes, - options, - stack - ); - if (!this._chance.bool({ likelihood })) { - return this._wrongData(typeof meta.builder(...args)); - } else { - return meta.builder(...args); - } - } - } - /** - * Resolve the value of the options or attributes if this has dependencies - * @param dependencies - option or attribute dependencies - * @param object - object with resolved attributes - * @param resolvedOptions - object with resolved options - * @param attributes - attributes object - * @param options - options object - * @param stack - stack of recursive calls - */ - private _BuildWithDependencies( - dependencies: (string | keyof T)[], - object: { [K in keyof T]?: T[K] }, - resolvedOptions: { [key: string]: any }, - attributes: { [K in keyof T]?: Entry }, - options: { [key: string]: Entry }, - stack: (keyof T | string)[] - ): any[] { - return dependencies.map(dep => { - if (stack.indexOf(dep) >= 0) { - throw new Crash(`Detect a dependency cycle: ${stack.concat([dep]).join(' -> ')}`, { - stack: stack.concat([dep]), - }); - } - let value: any; - if (object[dep as keyof T] !== undefined) { - value = object[dep as keyof T]; - } else if (resolvedOptions[dep as string] !== undefined) { - value = resolvedOptions[dep as string]; - } else if (options[dep as string]) { - resolvedOptions[dep as string] = this._Build( - options[dep as string], - object, - resolvedOptions, - attributes, - options, - stack.concat([dep]) - ); - value = resolvedOptions[dep as string]; - } else if (attributes[dep as keyof T]) { - object[dep as keyof T] = this._Build( - attributes[dep as keyof T] as Entry, - object, - resolvedOptions, - attributes, - options, - stack.concat([dep]) - ); - value = object[dep as keyof T]; - } - return value; - }); - } - /** - * Return Generator function if the argument is not a function - * @param generator - Generator function or value - */ - private _ReturnFunction( - generator?: Builder | DefaultValue - ): Builder { - if (generator instanceof Function) { - return generator; - } else { - return () => generator; - } - } - /** - * Return a Entry object with dependencies and builder function - * @param generatorOptions - Default value or generator function or dependencies - * @param generator - Generator function or value - */ - private _SafeType( - generatorOptions?: GeneratorOptions, - generator?: Builder - ): Entry { - let _dependencies: Dependencies | undefined; - let _builder: Builder; - if (generator === undefined) { - if (Array.isArray(generatorOptions)) { - throw new Crash('Generator function is required if dependencies are defined', { - attributeDependencies: generatorOptions, - }); - } - _dependencies = undefined; - _builder = this._ReturnFunction(generatorOptions); - } else { - if (Array.isArray(generatorOptions)) { - _dependencies = generatorOptions; - _builder = this._ReturnFunction(generator); - } else { - throw new Crash('Dependencies must be an array', { - attributeDependencies: generatorOptions, - }); - } - } - return { dependencies: _dependencies, builder: _builder }; - } - /** - * Generate wrong data, excluding the correct data type - * @param type - type of good data - */ - private _wrongData(type: string): any { - const _typeof: string[] = [ - 'undefined', - 'boolean', - 'number', - 'string', - 'object', - 'symbol', - 'bigint', - 'function', - ].filter(entry => entry !== type); - const selected = this._chance.pickone(_typeof); - switch (selected) { - case 'undefined': - return undefined; - case 'object': - return undefined; - case 'boolean': - return this._chance.bool(); - case 'number': - if (this._chance.bool()) { - return this._chance.floating(); - } else { - return this._chance.natural(); - } - case 'string': - return this._chance.string(); - default: - return null; - } - } - /** - * Create an object with standard Attributes from a key-value pairs object - * @param attributes - object containing attribute override key value pairs - */ - private _convertToAttributes(attributes: { [K in keyof T]?: T[K] }): { - [K in keyof T]?: Entry; - } { - const attrs: { [K in keyof T]?: Entry } = {}; - for (const attr in attributes) { - attrs[attr] = { dependencies: undefined, builder: () => attributes[attr] }; - } - return attrs; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Crash } from '@mdf.js/crash'; +import { Chance } from 'chance'; +import _, { merge } from 'lodash'; + +/** Type for attribute dependencies */ +export type Dependencies = (K | string)[]; +/** Type for function for attribute builder function */ +export type Builder = (...args: any) => T[K] | undefined; +/** Type for attribute default value */ +export type DefaultValue = T[K]; +/** Type for attribute value option */ +type GeneratorOptions = + | Builder + | DefaultValue + | Dependencies; +/** Interface for attribute generation */ +interface Entry { + dependencies?: Dependencies; + builder: Builder; +} +/** Interface for default object */ +export interface DefaultObject { + [key: string]: any; +} +/** Interface for default options */ +export interface DefaultOptions { + likelihood?: number; + [key: string]: any; +} + +/** Factory for building JavaScript objects, mostly useful for setting up test data */ +export class Factory< + T extends DefaultObject = DefaultObject, + R extends DefaultOptions = DefaultOptions, +> { + /** Options for programmatic generation of attributes */ + private _opts: { + [K in keyof R]: Entry; + }; + /** Attributes of this factory, based on a interface */ + private _attrs: { + [K in keyof T]: Entry; + }; + /** Auto incrementing sequence attribute */ + private _seques: { + [K in keyof T]?: number; + }; + /** Callback function array */ + private readonly _callbacks: ((object: T, options: R) => T)[]; + /** Chance object for probabilistic wrong value generation */ + private readonly _chance; + /** Create a new factory instance */ + public constructor() { + this._opts = {} as { [K in keyof R]: Entry }; + this._attrs = {} as { [K in keyof T]: Entry }; + this._seques = {}; + this._callbacks = []; + this._chance = new Chance(); + } + /** + * Define an attribute on this factory + * @param attr - Name of attribute + * @example + * ```typescript + * factory.attr('name'); + * ``` + */ + public attr(attr: K): Factory; + /** + * Define an attribute on this factory using a default value (e.g. a string or number) + * @param attr - Name of attribute + * @param defaultValue - Default value of attribute + * @example + * ```typescript + * factory.attr('name', 'John Doe'); + * ``` + */ + public attr(attr: K, defaultValue: DefaultValue): Factory; + /** + * Define an attribute on this factory using a generator function + * @param attr - Name of attribute + * @param generator - Value generator function + * @example + * ```typescript + * factory.attr('name', () => function() { return 'John Doe'; }); + * ``` + */ + public attr(attr: K, generator: Builder): Factory; + /** + * Define an attribute on this factory using a generator function and dependencies on options or + * other attributes + * @param attr - Name of attribute + * @param dependencies - Array of dependencies as option or attribute names that are used by the + * generator function to generate the value of this attribute + * @param generator - Value generator function. The generator function will be called with the + * resolved values of the dependencies as arguments. + * @example + * ```typescript + * factory.attr('name', ['firstName', 'lastName'], (firstName, lastName) => { + * return `${firstName} ${lastName}`; + * }); + * ``` + */ + public attr( + attr: K, + dependencies: Dependencies, + generator: Builder + ): Factory; + public attr( + attr: K, + generatorOptions?: GeneratorOptions, + builder?: Builder + ): Factory { + this._attrs[attr] = this._SafeType(generatorOptions, builder); + return this; + } + /** + * Define multiple attributes on this factory using a default value (e.g. a string or number) or + * generator function. If you need to define dependencies on options or other attributes, use the + * `attr` method instead. + * @param attributes - Object with multiple attributes + * @example + * ```typescript + * factory.attrs({ + * name: 'John Doe', + * age: function() { return 21; }, + * }); + * ``` + */ + public attrs(attributes: { + [K in keyof T]: DefaultValue | Builder; + }): Factory { + for (const attr in attributes) { + if (attributes.hasOwnProperty(attr)) { + this.attr(attr, attributes[attr]); + } + } + return this; + } + /** + * Define an option for this factory using a default value. Options are values that are not + * directly used in the generated object, but can be used to influence the generation process. + * For example, you could define an option `withAddress` that, when set to `true`, would generate + * an address and add it to the generated object. Like attributes, options can have dependencies + * on other options but not on attributes. + * @param opt - Name of option + * @param defaultValue - Default value of option + * @example + * ```typescript + * factory.option('withAddress', false); + * ``` + */ + public option(opt: K, defaultValue: DefaultValue): Factory; + /** + * Define an option for this factory using a generator function. Options are values that are not + * directly used in the generated object, but can be used to influence the generation process. + * For example, you could define an option `withAddress` that, when set to `true`, would generate + * an address and add it to the generated object. Like attributes, options can have dependencies + * on other options but not on attributes. + * @param opt - Name of option + * @param generator - Value generator function + * @example + * ```typescript + * factory.option('withAddress', () => function() { return false; }); + * ``` + */ + public option(opt: K, generator: Builder): Factory; + /** + * Define an option for this factory using a generator function with dependencies in other + * options. Options are values that are not directly used in the generated object, but can be + * used to influence the generation process. For example, you could define an option + * `withAddress` that, when set to `true`, would generate an address and add it to the generated + * object. Like attributes, options can have dependencies on other options but not on attributes. + * @param opt - Name of option + * @param dependencies - Array of dependencies as option names that are used by the generator + * function to generate the value of this option + * @param generator - Value generator function with dependencies in other options. The generator + * function will be called with the resolved values of the dependencies as arguments. + */ + public option( + opt: K, + dependencies: Dependencies, + generator: Builder + ): Factory; + public option( + opt: K, + generatorOptions: GeneratorOptions, + builder?: Builder + ): Factory { + this._opts[opt] = this._SafeType(generatorOptions, builder); + return this; + } + /** + * Define an auto incrementing sequence attribute of the object. Default value is 1. + * @param attr - Name of attribute + * @example + * ```typescript + * factory.sequence('id'); + * ``` + */ + public sequence(attr: K): Factory; + /** + * Define an auto incrementing sequence attribute of the object where the sequence value is + * generated by a generator function that is called with the current sequence value as argument. + * @param attr - Name of attribute + * @param generator - Value generator function + * @example + * ```typescript + * factory.sequence('id', (i) => function() { return i + 11; }); + * ``` + */ + public sequence(attr: K, generator: Builder): Factory; + /** + * Define an auto incrementing sequence attribute of the object where the sequence value is + * generated by a generator function that is called with the current sequence value as argument + * and dependencies on options or other attributes. + * @param attr - Name of attribute + * @param dependencies - Array of dependencies as option or attribute names that are used by the + * generator function to generate the value of the sequence attribute + * @param generator - Value generator function + * @example + * ```typescript + * factory.sequence('id', ['idPrefix'], (i, idPrefix) => function() { + * return `${idPrefix}${i}`; + * }); + * ``` + */ + public sequence( + attr: K, + dependencies: (K | string)[], + generator: Builder + ): Factory; + public sequence( + attr: K, + generatorOptions?: GeneratorOptions, + builder?: Builder + ): Factory { + const _attribute = this._SafeType(generatorOptions, builder); + if (generatorOptions === undefined && builder === undefined) { + _attribute.builder = i => i + 1; + _attribute.dependencies = []; + } + + return this.attr(attr, _attribute.dependencies || [], (...args: any[]) => { + const value = _attribute.builder(this._seques[attr] ?? 0, ...args); + this._seques[attr] = value; + return value; + }); + } + /** + * Register a callback function to be called after the object is generated. The callback function + * receives the generated object as first argument and the resolved options as second argument. + * @param callback - Callback function + * @example + * ```typescript + * factory.after((user) => { + * user.name = user.name.toUpperCase(); + * }); + * ``` + */ + public after(callback: (object: T, options: R) => T): Factory { + this._callbacks.push(callback); + return this; + } + /** + * Returns an object that is generated by the factory. + * The optional option `likelihood` is a number between 0 and 100 that defines the probability + * that the generated object contains wrong data. This is useful for testing if your code can + * handle wrong data. The default value is 100, which means that the generated object always + * contains correct data. + * @param attributes - object containing attribute override key value pairs + * @param options - object containing option key value pairs + */ + public build( + attributes: { [K in keyof T]?: T[K] } = {}, + options: { likelihood?: number; [key: string]: any } = { likelihood: 100 } + ): T { + if (options && options['likelihood'] === undefined) { + options = { ...options, likelihood: 100 }; + } + if ( + typeof options['likelihood'] !== 'number' || + options['likelihood'] < 0 || + options['likelihood'] > 100 + ) { + throw new Crash('Likelihood must be a number between 0 and 100', { + likelihood: options['likelihood'], + }); + } + const _attributes = _.merge(_.cloneDeep(this._attrs), this._convertToAttributes(attributes)); + const _options = _.merge(this._opts, this._ConvertToOptions(options)); + let returnableObject: { [K in keyof T]?: T[K] } = {}; + const resolvedOptions: { [key: string]: any } = {}; + for (const attr in this._attrs) { + const stack: (keyof T | string)[] = []; + returnableObject[attr] = this._Build( + _attributes[attr] as Entry, + returnableObject, + resolvedOptions, + _attributes, + _options, + stack, + options['likelihood'] + ); + } + if ( + (Object.keys(this._attrs).length === 0 || Object.keys(resolvedOptions).length === 0) && + Object.keys(_options).length > 0 + ) { + for (const opts in _options) { + const stack: (keyof T | string)[] = []; + resolvedOptions[opts] = this._Build( + _options[opts], + returnableObject, + resolvedOptions, + _attributes, + _options, + stack + ); + } + } + for (const callback of this._callbacks) { + const obj = callback(returnableObject as T, resolvedOptions as R); + if (obj !== undefined) { + returnableObject = obj; + } + } + return returnableObject as T; + } + /** + * Returns an array of objects that are generated by the factory. + * The optional option `likelihood` is a number between 0 and 100 that defines the probability + * that the generated object contains wrong data. This is useful for testing if your code can + * handle wrong data. The default value is 100, which means that the generated object always + * contains correct data. + * @param size - number of objects to generate + * @param attributes - object containing attribute override key value pairs + * @param options - object containing option key value pairs + * @example + * ```typescript + * factory.buildList(3, { name: 'John Doe' }); + * ``` + */ + public buildList( + size: number, + attributes: { [K in keyof T]?: T[K] } = {}, + options: { likelihood?: number; [key: string]: any } = { likelihood: 100 } + ): T[] { + const objs = []; + for (let i = 0; i < size; i++) { + objs.push(this.build(attributes, options)); + } + return objs; + } + /** + * Extend this factory with another factory. The attributes and options of the other factory are + * merged into this factory. If an attribute or option with the same name already exists, it is + * overwritten. + * @param factory - Factory to extend this factory with + */ + public extend

                          , J extends Partial>(factory: Factory): Factory { + Object.assign(this._attrs, factory._attrs); + Object.assign(this._opts, factory._opts); + this._callbacks.push(...factory._callbacks.map(this.wrapCallback)); + return this; + } + /** Reset all the sequences of this factory */ + public reset(): void { + this._seques = {}; + } + /** + * Wrap a callback function to add type safety and avoid lost of data of extended factories + * @param callback - Callback function + */ + private readonly wrapCallback =

                          , J extends Partial>( + callback: (object: P, options: J) => P + ): ((object: T, options: R) => T) => { + return (object: T, options: R) => { + const result = callback(object as unknown as P, options as unknown as J); + return merge(object, result) as T; + }; + }; + /** + * Create an object with standard Options from a key-value pairs object + * @param options - object containing option key value pairs + */ + private _ConvertToOptions(options: { [key: string]: any }): { [key: string]: Entry } { + const opts: { [key: string]: Entry } = {}; + for (const opt in options) { + opts[opt] = { dependencies: undefined, builder: () => options[opt] }; + } + return opts; + } + /** + * Resolve the value of the options or attributes + * @param meta - metadata information of option or attribute + * @param object - object with resolved attributes + * @param resolvedOptions - object with resolved options + * @param attributes - attributes object + * @param options - options object + * @param stack - stack of recursive calls + */ + private _Build( + meta: Entry, + object: { [K in keyof T]?: T[K] }, + resolvedOptions: { [key: string]: any }, + attributes: { [K in keyof T]?: Entry }, + options: { [key: string]: Entry }, + stack: (keyof T | string)[], + likelihood = 100 + ): any { + if (!meta) { + throw new Crash('Error in factory build process', { + meta, + object, + resolvedOptions, + attributes, + options, + stack, + }); + } + if (!meta.dependencies) { + if (!this._chance.bool({ likelihood })) { + return this._wrongData(typeof meta.builder()); + } else { + return meta.builder(); + } + } else { + const args = this._BuildWithDependencies( + meta.dependencies, + object, + resolvedOptions, + attributes, + options, + stack + ); + if (!this._chance.bool({ likelihood })) { + return this._wrongData(typeof meta.builder(...args)); + } else { + return meta.builder(...args); + } + } + } + /** + * Resolve the value of the options or attributes if this has dependencies + * @param dependencies - option or attribute dependencies + * @param object - object with resolved attributes + * @param resolvedOptions - object with resolved options + * @param attributes - attributes object + * @param options - options object + * @param stack - stack of recursive calls + */ + private _BuildWithDependencies( + dependencies: (string | keyof T)[], + object: { [K in keyof T]?: T[K] }, + resolvedOptions: { [key: string]: any }, + attributes: { [K in keyof T]?: Entry }, + options: { [key: string]: Entry }, + stack: (keyof T | string)[] + ): any[] { + return dependencies.map(dep => { + if (stack.indexOf(dep) >= 0) { + throw new Crash(`Detect a dependency cycle: ${stack.concat([dep]).join(' -> ')}`, { + stack: stack.concat([dep]), + }); + } + let value: any; + if (object[dep as keyof T] !== undefined) { + value = object[dep as keyof T]; + } else if (resolvedOptions[dep as string] !== undefined) { + value = resolvedOptions[dep as string]; + } else if (options[dep as string]) { + resolvedOptions[dep as string] = this._Build( + options[dep as string], + object, + resolvedOptions, + attributes, + options, + stack.concat([dep]) + ); + value = resolvedOptions[dep as string]; + } else if (attributes[dep as keyof T]) { + object[dep as keyof T] = this._Build( + attributes[dep as keyof T] as Entry, + object, + resolvedOptions, + attributes, + options, + stack.concat([dep]) + ); + value = object[dep as keyof T]; + } + return value; + }); + } + /** + * Return Generator function if the argument is not a function + * @param generator - Generator function or value + */ + private _ReturnFunction( + generator?: Builder | DefaultValue + ): Builder { + if (generator instanceof Function) { + return generator; + } else { + return () => generator; + } + } + /** + * Return a Entry object with dependencies and builder function + * @param generatorOptions - Default value or generator function or dependencies + * @param generator - Generator function or value + */ + private _SafeType( + generatorOptions?: GeneratorOptions, + generator?: Builder + ): Entry { + let _dependencies: Dependencies | undefined; + let _builder: Builder; + if (generator === undefined) { + if (Array.isArray(generatorOptions)) { + throw new Crash('Generator function is required if dependencies are defined', { + attributeDependencies: generatorOptions, + }); + } + _dependencies = undefined; + _builder = this._ReturnFunction(generatorOptions); + } else if (Array.isArray(generatorOptions)) { + _dependencies = generatorOptions; + _builder = this._ReturnFunction(generator); + } else { + throw new Crash('Dependencies must be an array', { + attributeDependencies: generatorOptions, + }); + } + + return { dependencies: _dependencies, builder: _builder }; + } + /** + * Generate wrong data, excluding the correct data type + * @param type - type of good data + */ + private _wrongData(type: string): any { + const _typeof: string[] = [ + 'undefined', + 'boolean', + 'number', + 'string', + 'object', + 'symbol', + 'bigint', + 'function', + ].filter(entry => entry !== type); + const selected = this._chance.pickone(_typeof); + switch (selected) { + case 'undefined': + return undefined; + case 'object': + return undefined; + case 'boolean': + return this._chance.bool(); + case 'number': + if (this._chance.bool()) { + return this._chance.floating(); + } else { + return this._chance.natural(); + } + case 'string': + return this._chance.string(); + default: + return null; + } + } + /** + * Create an object with standard Attributes from a key-value pairs object + * @param attributes - object containing attribute override key value pairs + */ + private _convertToAttributes(attributes: { [K in keyof T]?: T[K] }): { + [K in keyof T]?: Entry; + } { + const attrs: { [K in keyof T]?: Entry } = {}; + for (const attr in attributes) { + attrs[attr] = { dependencies: undefined, builder: () => attributes[attr] }; + } + return attrs; + } +} diff --git a/packages/api/file-flinger/.eslintignore b/packages/api/file-flinger/.eslintignore new file mode 100644 index 00000000..1fb0aaeb --- /dev/null +++ b/packages/api/file-flinger/.eslintignore @@ -0,0 +1,3 @@ +.eslintrc.js +stryker.conf.js +jest.config.js \ No newline at end of file diff --git a/packages/api/file-flinger/.eslintrc.js b/packages/api/file-flinger/.eslintrc.js new file mode 100644 index 00000000..6043be8e --- /dev/null +++ b/packages/api/file-flinger/.eslintrc.js @@ -0,0 +1,7 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +module.exports = require('@mdf.js/repo-config').getEslintConfig(); diff --git a/packages/api/file-flinger/README.md b/packages/api/file-flinger/README.md new file mode 100644 index 00000000..a78c938f --- /dev/null +++ b/packages/api/file-flinger/README.md @@ -0,0 +1,456 @@ +# **@mdf.js/file-flinger** + +[![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) +[![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) +[![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) + + + +

                          +

                          + netin +
                          +

                          + +

                          Mytra Development Framework - @mdf.js/file-flinger

                          +
                          Module designed to facilitate data file processing for cold path ingestion
                          + + + +*** + +## **Table of contents** + +- [**@mdf.js/file-flinger**](#mdfjsfile-flinger) + - [**Table of contents**](#table-of-contents) + - [**Introduction**](#introduction) + - [**Installation**](#installation) + - [**Use**](#use) + - [**Creating a Pusher**](#creating-a-pusher) + - [**Event Handling**](#event-handling) + - [**FileFlinger Configuration and Lifecycle**](#fileflinger-configuration-and-lifecycle) + - [**Metrics and Health Checks**](#metrics-and-health-checks) + - [**Keygen: Detailed Explanation of Key Generation**](#keygen-detailed-explanation-of-key-generation) + - [**Overview**](#overview) + - [**Placeholders**](#placeholders) + - [**Predefined Placeholders**](#predefined-placeholders) + - [**Custom Placeholders**](#custom-placeholders) + - [**Keygen Options**](#keygen-options) + - [**Examples**](#examples) + - [**Example 1: Default Behavior**](#example-1-default-behavior) + - [**Example 2: Custom File and Key Patterns**](#example-2-custom-file-and-key-patterns) + - [**Example 3: Applying Default Values**](#example-3-applying-default-values) + - [**Example 4: Using Date Placeholders**](#example-4-using-date-placeholders) + - [**Example 5: Advanced Customization**](#example-5-advanced-customization) + - [**Error Handling**](#error-handling) + - [**Tips and Best Practices**](#tips-and-best-practices) + - [**License**](#license) + +## **Introduction** + +**@mdf.js/file-flinger** is a robust module within the `@mdf.js` ecosystem, designed to facilitate customized data file processing workflows for cold path ingestion. It provides a versatile framework for constructing file processing pipelines, enabling developers to define custom pushers to deliver data files to various destinations. + +Before delving into the documentation, it is essential to understand the core concepts within `@mdf.js/file-flinger`: + +- **Keygen**: A key generation utility that creates unique identifiers for processed files based on customizable patterns. This feature is crucial for tracking and managing data files across the platform by generating consistent and meaningful identifiers. +- **Pusher**: A pusher is a component that sends processed files to a specific destination. Developers can create custom pushers to integrate with various data storage solutions, such as databases, cloud storage, or data lakes. The file-flinger module supports multiple pushers, allowing users to define different destinations for processed files concurrently. +- **Watcher**: The watcher module monitors directories for incoming files and triggers processing workflows when new files are detected. It plays a pivotal role in automating data ingestion tasks by initiating processing as soon as new data arrives. +- **Post-processing tasks**: After processing a file, it is possible to execute a set of tasks to clean up the file system, move files to another location, archive them, or perform other operations. This post-processing can be different for completed and failed files, providing flexibility in handling different outcomes. + +## **Installation** + +- **npm**: + + ```bash + npm install @mdf.js/file-flinger + ``` + +- **yarn**: + + ```bash + yarn add @mdf.js/file-flinger + ``` + +## **Use** + +### **Creating a Pusher** + +To build a file processing workflow, you need to create custom pushers that send processed files to specific destinations. A pusher is responsible for delivering files to storage systems like databases, cloud storage, or data lakes. + +To create a pusher, you need to define a class that implements the `Pusher` interface. This interface extends `Layer.App.Resource`, which provides methods and properties for managing the resource lifecycle and health status. + +When implementing a pusher, you should ensure the following: + +- **Lifecycle Methods**: Implement `start()`, `stop()`, and `close()` methods to manage the pusher's lifecycle. +- **Push Method**: Implement the `push(filePath: string, key: string)` method, which handles the logic to send the file to the destination using the provided file path and key. +- **Metrics**: Provide a `metrics` getter that returns a Prometheus `Registry` containing the pusher's metrics. +- **Health Status**: Provide `status` and `checks` getters that return the pusher's health status and checks, which are crucial for monitoring the pusher's health. + +If you are using the `@mdf.js` framework to create your pusher, you can integrate the pusher's health information with the provider's health information. + +Here is an example of a custom pusher class: + +```typescript +import { EventEmitter } from 'events'; +import { Pusher } from '@mdf.js/file-flinger'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +// Class that implements the Pusher interface +class MyCustomPusher extends EventEmitter implements Pusher { + /** Constructor */ + constructor() { + super(); + } + + /** + * Push the file to the storage + * @param filePath - The file path to push + * @param key - The key to use + */ + public async push(filePath: string, key: string): Promise { + // Implementation of file pushing logic + } + + /** Start the pusher and the underlying provider */ + public async start(): Promise { + // Initialization logic + } + + /** Stop the pusher and the underlying provider */ + public async stop(): Promise { + // Graceful shutdown logic + } + + /** Stop the pusher and the underlying provider and clean the resources */ + public async close(): Promise { + // Cleanup logic + } + + /** Prometheus registry to store the metrics of the pusher */ + public get metrics(): Registry { + // Return Prometheus registry with pusher metrics + return new Registry(); + } + + /** Pusher health status */ + public get status(): Health.Status { + // Return health status + return 'pass'; + } + + /** Pusher health checks */ + public get checks(): Health.Checks { + // Return object with health checks + return {}; + } +} +``` + +### **Event Handling** + +The `FileFlinger` class extends `EventEmitter` and emits several events that you can listen to: + +- `error`: Emitted when the component detects an error. + + ```typescript + fileFlinger.on('error', (error) => { + console.error('An error occurred:', error); + }); + ``` + +- `status`: Emitted when the component's status changes. + + ```typescript + fileFlinger.on('status', (status) => { + console.log('FileFlinger status:', status); + }); + ``` + +### **FileFlinger Configuration and Lifecycle** + +To instantiate a `FileFlinger`, you need to provide a name and an options object that configures its behavior. The options include: + +- **`pushers`**: An array of pushers that will be used to send files to their destinations. +- **`watchPath`**: The path or array of paths to monitor for incoming files. +- **`filePattern`** (default: `undefined`): A glob pattern or custom pattern to match the files to be processed. +- **`keyPattern`** (default: `{_filename}`): A pattern used by the key generator (`Keygen`) to create unique keys for the files. +- **`defaultValues`** (default: `{}`): An object containing default values for placeholders used in patterns. +- **`cwd`** (default: `undefined`): The base directory for relative paths. +- **`maxErrors`** (default: `10`): The maximum number of errors to store in the error list. +- **`retryDelay`** (default: `30000`): Delay in milliseconds between retries for failed file processing operations. +- **`archiveFolder`** (default: `undefined`): The directory where processed files will be moved if the post-processing strategy is `archive` or `zip`. +- **`deadLetterFolder`** (default: `undefined`): The directory where files with processing errors will be moved if the error strategy is `dead-letter`. +- **`postProcessingStrategy`** (default: `'delete'`): Strategy for handling files after successful processing. Options are: + - `'delete'`: Delete the file. + - `'archive'`: Move the file to the `archiveFolder`. + - `'zip'`: Compress the file and move it to the `archiveFolder`. +- **`errorStrategy`** (default: `'delete'`): Strategy for handling files that encountered errors during processing. Options are: + - `'delete'`: Delete the file. + - `'ignore'`: Leave the file as is. + - `'dead-letter'`: Move the file to the `deadLetterFolder`. +- **`retryOptions`**: Configuration for retrying file operations. Includes: + - **`attempts`** (default: `3`): Number of retry attempts. + - **`maxWaitTime`** (default: `60000`): Maximum total wait time in milliseconds between retries. + - **`timeout`** (default: `10000`): Timeout in milliseconds for each retry attempt. + - **`waitTime`** (default: `1000`): Initial wait time in milliseconds between retries, which may be increased based on a backoff strategy. + +Here's how to create a `FileFlinger` instance with custom options: + +```typescript +import { FileFlinger } from '@mdf.js/file-flinger'; + +const fileFlinger = new FileFlinger('MyFileFlinger', { + pushers: [/* Your custom pushers */], + watchPath: '/path/to/watch', + filePattern: '{sensor}_{measurement}_{date}.jsonl', + keyPattern: '{sensor}/{measurement}/{date}', + defaultValues: {}, + cwd: process.cwd(), + maxErrors: 10, + retryDelay: 30000, + archiveFolder: '/path/to/archive', + deadLetterFolder: '/path/to/dead-letter', + postProcessingStrategy: 'archive', + errorStrategy: 'dead-letter', + retryOptions: { + attempts: 3, + maxWaitTime: 60000, + timeout: 10000, + waitTime: 1000, + }, +}); +``` + +To manage the lifecycle of the `FileFlinger`, you can use the following methods: + +- `start(): Promise`: Starts the `FileFlinger`, initializing all watchers and pushers, and begins processing files as they arrive. +- `stop(): Promise`: Stops the `FileFlinger`, gracefully shutting down all watchers and pushers. +- `close(): Promise`: Stops the `FileFlinger` and cleans up all resources, including closing any open file handles or network connections. + +Example: + +```typescript +// Start the FileFlinger +await fileFlinger.start(); + +// The FileFlinger is now monitoring for files and processing them. + +// When you need to stop the FileFlinger +await fileFlinger.stop(); + +// If you want to completely close and clean up resources +await fileFlinger.close(); +``` + +### **Metrics and Health Checks** + +The `FileFlinger` class includes a Prometheus `Registry` to store metrics related to the file processing pipeline. These metrics can be used to monitor the performance and health of the system. + +Default metrics included in the `FileFlinger` are: + +- `api_all_job_processed_total`: The total number of jobs processed, labeled by `type`. +- `api_all_errors_job_processing_total`: The total number of errors encountered during job processing, labeled by `type`. +- `api_all_job_in_processing_total`: The number of jobs currently being processed, labeled by `type`. +- `api_publishing_job_duration_milliseconds`: The duration of file processing jobs in milliseconds, labeled by `type`. + +The `type` label typically represents the key generated for the file, allowing you to categorize metrics by file type or other meaningful identifiers. + +Pushers should also provide metrics and health information. They should implement the `metrics`, `status`, and `checks` properties: + +- **`metrics`**: Returns a Prometheus `Registry` with the pusher's metrics. +- **`status`**: Returns the health status of the pusher (`'pass'` or `'fail'`). +- **`checks`**: Returns an object containing detailed health checks for the pusher. + +You can access the `FileFlinger`'s metrics and health information: + +```typescript +// Access metrics +const metricsRegistry = fileFlinger.metrics; + +// Access health status and checks +const fileFlingerStatus = fileFlinger.status; +const fileFlingerChecks = fileFlinger.checks; +``` + +### **Keygen: Detailed Explanation of Key Generation** + +The `Keygen` utility is responsible for generating unique and meaningful identifiers (keys) for processed files. These keys are used to identify and track files within the system and are crucial for organizing data in storage destinations. + +#### **Overview** + +The key generation process involves: + +1. **Parsing the File Name**: Extract placeholders from the file name using a specified `filePattern`. +2. **Generating Predefined Placeholders**: Create a set of predefined placeholders based on the current date and time. +3. **Merging Placeholders**: Combine default values, parsed placeholders, and predefined placeholders into a single set. +4. **Generating the Key**: Replace placeholders in the `keyPattern` with actual values from the merged placeholders to produce the final key. + +#### **Placeholders** + +Placeholders are enclosed in curly braces `{}` and are used in both the `filePattern` and `keyPattern`. They are replaced with actual values during key generation. + +##### **Predefined Placeholders** + +The following placeholders are available by default: + +- `{_filename}`: The base name of the file without its extension. +- `{_extension}`: The file extension (including the dot), e.g., `.jsonl`. +- `{_timestamp}`: The current timestamp in milliseconds since the Unix epoch. +- `{_date}`: The current date in `YYYY-MM-DD` format. +- `{_time}`: The current time in `HH:mm:ss` format. +- `{_datetime}`: The current date and time in `YYYY-MM-DD_HH-mm-ss` format. +- `{_year}`: The current year as a four-digit number. +- `{_month}`: The current month as a two-digit number (01-12). +- `{_day}`: The current day of the month as a two-digit number (01-31). +- `{_hour}`: The current hour as a two-digit number (00-23). +- `{_minute}`: The current minute as a two-digit number (00-59). +- `{_second}`: The current second as a two-digit number (00-59). + +##### **Custom Placeholders** + +You can define custom placeholders by specifying them in the `filePattern`. These placeholders extract corresponding values from the file name. + +**Example**: + +- **File Name**: `sensor1_temperature_2023-10-24.jsonl` +- **File Pattern**: `{sensor}_{measurement}_{date}.jsonl` +- **Extracted Placeholders**: `sensor`, `measurement`, `date` + +#### **Keygen Options** + +The `Keygen` class accepts an `options` object to customize its behavior: + +- **`filePattern`**: A pattern used to parse the file name and extract placeholders. +- **`keyPattern`**: A pattern used to generate the key by replacing placeholders with actual values. +- **`defaultValues`**: An object containing default values for placeholders that may not be present in the file name. + +**Default Options**: + +```typescript +const DEFAULT_KEY_GEN_OPTIONS: Required = { + filePattern: '*', // Matches any file name + keyPattern: '{_filename}', // Uses the file name without extension as the key + defaultValues: {}, // No default values provided +}; +``` + +#### **Examples** + +##### **Example 1: Default Behavior** + +**Description**: Generate a key using default settings. + +```yaml +# filePattern: undefined +keyPattern: '{_filename}' +# defaultValues: {} +``` + +- **Filename**: `myfile.txt` +- **Key**: `myfile` + +**Explanation**: + +- Since `filePattern` is `undefined`, any file name matches. +- The `keyPattern` `{_filename}` uses the file name without the extension. +- The key generated is `'myfile'`. + +##### **Example 2: Custom File and Key Patterns** + +**Description**: Generate a key by extracting custom placeholders from the file name. + +```yaml +filePattern: '{sensor}_{measurement}_{date}.jsonl' +keyPattern: '{sensor}/{measurement}/{date}' +# defaultValues: {} +``` + +- **Filename**: `sensor1_temperature_2023-10-24.jsonl` +- **Key**: `sensor1/temperature/2023-10-24` + +**Explanation**: + +- The `filePattern` extracts `sensor`, `measurement`, and `date` from the file name. +- The `keyPattern` constructs the key using these placeholders. + +##### **Example 3: Applying Default Values** + +**Description**: Use default values for placeholders not present in the file name. + +```yaml +filePattern: '{sensor}_{measurement}_{date}.jsonl' +keyPattern: '{sensor}/{measurement}/{date}/{location}' +defaultValues: + location: 'defaultLocation' +``` + +- **Filename**: `sensor1_temperature_2023-10-24.jsonl` +- **Key**: `sensor1/temperature/2023-10-24/defaultLocation` + +**Explanation**: + +- The `location` placeholder is not present in the file name. +- The `defaultValues` provide a value for `location`. + +##### **Example 4: Using Date Placeholders** + +**Description**: Generate a key that includes current date components. + +```yaml +filePattern: '{sensor}_{measurement}.jsonl' +keyPattern: '{sensor}/{measurement}/{_year}/{_month}/{_day}' +# defaultValues: {} +``` + +- **Filename**: `sensor1_temperature.jsonl` +- **Key**: `sensor1/temperature/2024/11/03` + +**Explanation**: + +- The placeholders `{_year}`, `{_month}`, `{_day}` are replaced with the current date components. + +##### **Example 5: Advanced Customization** + +**Description**: Generate a key using complex file patterns and default values. + +```yaml +filePattern: '{sensor}_{measurement}_{year}-{month}-{day}_{end}.jsonl' +keyPattern: '{sensor}/{measurement}/{year}/{month}/{day}/data_{source}' +defaultValues: + source: 'myFileFlinger1' +``` + +- **Filename**: `mySensor_flowMeter1_2024-12-30_2024-12-31.jsonl` +- **Key**: `mySensor/flowMeter1/2024/12/30/data_myFileFlinger1` + +**Explanation**: + +- Custom placeholders `sensor`, `measurement`, `year`, `month`, `day`, and `end` are extracted from the file name. +- The `source` placeholder is provided via `defaultValues`. + +#### **Error Handling** + +During key generation, some errors can occur. These errors are emitted as `error` events and can be handled by listening to the `FileFlinger`'s `error` event. + +- **Filename Does Not Match Pattern**: If the file name does not match the `filePattern`, an error is emitted. + + **Error Message**: `'Filename [invalid_filename.jsonl] does not match the pattern [{sensor}_{measurement}_{date}.jsonl]'` + +- **Placeholder Not Found in Values**: If a placeholder in the `keyPattern` is not found in the merged placeholders, an error is emitted. + + **Error Message**: `'Error generating a key based on pattern [{sensor}/{measurement}/{date}/{unknown}] for file [sensor1_temperature_2023-10-24.jsonl]: Placeholder [unknown] not found in values'` + +#### **Tips and Best Practices** + +- **Define Both `filePattern` and `keyPattern`**: Explicitly specify these patterns to ensure keys are generated as expected. +- **Ensure Consistency**: Make sure placeholders used in `keyPattern` are either extracted from the file name, provided in `defaultValues`, or are predefined placeholders. +- **Test Your Patterns**: Validate your patterns with various file names to ensure they work correctly. +- **Handle Errors Gracefully**: Implement error handling for key generation errors to prevent processing failures. + +## **License** + +Copyright 2024 Mytra Control S.L. All rights reserved. + +Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at . diff --git a/packages/api/file-flinger/jest.config.js b/packages/api/file-flinger/jest.config.js new file mode 100644 index 00000000..267e07db --- /dev/null +++ b/packages/api/file-flinger/jest.config.js @@ -0,0 +1,7 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +module.exports = require('@mdf.js/repo-config').getJestConfig(__dirname); diff --git a/packages/api/file-flinger/package.json b/packages/api/file-flinger/package.json new file mode 100644 index 00000000..07ac9a09 --- /dev/null +++ b/packages/api/file-flinger/package.json @@ -0,0 +1,57 @@ +{ + "name": "@mdf.js/file-flinger", + "version": "0.0.1", + "description": "MMS - API Core - File flinger library", + "keywords": [ + "NodeJS", + "MMS", + "API", + "file", + "flinger", + "upload", + "push" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/file-flinger" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/file-flinger/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/tasks": "*", + "@mdf.js/utils": "*", + "chokidar": "^4.0.1", + "lodash": "^4.17.21", + "prom-client": "^15.1.3", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/file-flinger/src/FileFinger.test.ts b/packages/api/file-flinger/src/FileFinger.test.ts new file mode 100644 index 00000000..9fda964d --- /dev/null +++ b/packages/api/file-flinger/src/FileFinger.test.ts @@ -0,0 +1,362 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import fs from 'fs'; +import { setTimeout as wait } from 'timers/promises'; +import { v4 } from 'uuid'; +import { FileFlinger } from './FileFlinger'; +import { Pusher } from './pusher'; + +// @ts-expect-error - Mocking the pusher +const pusher = { + name: 'test', + componentId: v4(), + push: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + close: jest.fn(), +} as Pusher; + +describe('#FileFlinger', () => { + describe('#Happy path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should create a new instance of FileFlinger', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + const fileFlinger = new FileFlinger('myFlinger', { + pushers: [pusher], + }); + expect(fileFlinger).toBeInstanceOf(FileFlinger); + }); + it(`Should stop/start/close the FileFlinger`, async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + const fileFlinger = new FileFlinger('myFlinger', { + watchPath: `${__dirname}/FileFlinger.test.ts`, + pushers: [pusher], + }); + expect(fileFlinger.checks).toEqual({ + 'myFlinger:errors': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: [], + observedUnit: 'observed errors', + time: undefined, + output: undefined, + }, + ], + 'myFlinger:status': [ + { + status: 'fail', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: 'fail', + observedUnit: 'status', + time: expect.any(String), + output: 'Watcher is not ready', + }, + ], + 'myFlinger:watcher': [ + { + status: 'warn', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: undefined, + observedUnit: 'watched files', + time: expect.any(String), + output: 'Watcher is not started', + }, + ], + 'test:lastOperation': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'plug', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: undefined, + output: undefined, + }, + ], + 'myFlinger:fileTasks': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'fileTasks', + observedValue: [], + observedUnit: 'errored files', + time: expect.any(String), + output: undefined, + }, + ], + }); + expect(fileFlinger.status).toEqual('fail'); + + await fileFlinger.start(); + await wait(150); + expect(fileFlinger.checks).toEqual({ + 'myFlinger:errors': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: [], + observedUnit: 'observed errors', + time: undefined, + output: undefined, + }, + ], + 'myFlinger:status': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: 'pass', + observedUnit: 'status', + time: expect.any(String), + output: undefined, + }, + ], + 'myFlinger:watcher': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: expect.any(Object), + observedUnit: 'watched files', + time: expect.any(String), + output: undefined, + }, + ], + 'test:lastOperation': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'plug', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: expect.any(String), + output: undefined, + }, + ], + 'myFlinger:fileTasks': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'fileTasks', + observedValue: [], + observedUnit: 'errored files', + time: expect.any(String), + output: undefined, + }, + ], + }); + expect(fileFlinger.status).toEqual('pass'); + expect(pusher.start).toHaveBeenCalled(); + + await fileFlinger.stop(); + expect(fileFlinger.checks).toEqual({ + 'myFlinger:errors': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: [], + observedUnit: 'observed errors', + time: undefined, + output: undefined, + }, + ], + 'myFlinger:status': [ + { + status: 'fail', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: 'fail', + observedUnit: 'status', + time: expect.any(String), + output: 'Watcher is not ready', + }, + ], + 'myFlinger:watcher': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: expect.any(Object), + observedUnit: 'watched files', + time: expect.any(String), + output: undefined, + }, + ], + 'test:lastOperation': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'plug', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: expect.any(String), + output: undefined, + }, + ], + 'myFlinger:fileTasks': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'fileTasks', + observedValue: [], + observedUnit: 'errored files', + time: expect.any(String), + output: undefined, + }, + ], + }); + expect(fileFlinger.status).toEqual('fail'); + expect(pusher.stop).toHaveBeenCalled(); + + await fileFlinger.close(); + expect(fileFlinger.checks).toEqual({ + 'myFlinger:errors': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: [], + observedUnit: 'observed errors', + time: undefined, + output: undefined, + }, + ], + 'myFlinger:status': [ + { + status: 'fail', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: 'fail', + observedUnit: 'status', + time: expect.any(String), + output: 'Watcher is not ready', + }, + ], + 'myFlinger:watcher': [ + { + status: 'warn', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: undefined, + observedUnit: 'watched files', + time: expect.any(String), + output: 'Watcher is not started', + }, + ], + 'test:lastOperation': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'plug', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: expect.any(String), + output: undefined, + }, + ], + 'myFlinger:fileTasks': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'fileTasks', + observedValue: [], + observedUnit: 'errored files', + time: expect.any(String), + output: undefined, + }, + ], + }); + expect(fileFlinger.status).toEqual('fail'); + expect(pusher.close).toHaveBeenCalled(); + }); + it(`Should try to process a file if the watcher emit an 'add' event`, async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + const fileFlinger = new FileFlinger('myFlinger', { + watchPath: `${__dirname}/FileFlinger.test.ts`, + pushers: [pusher], + }); + await fileFlinger.start(); + // @ts-expect-error - Mocking the processFile + jest.spyOn(fileFlinger.engine, 'processFile').getMockImplementation((file: string) => {}); + // @ts-expect-error - Mocking the watcher + fileFlinger.watcher.emit('add', `${__dirname}/FileFlinger.test.ts`); + // @ts-expect-error - Mocking the processFile + expect(fileFlinger.engine.processFile).toHaveBeenCalled(); + }); + }); + describe('#Sad path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should throw an error if no watchPath are passed', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + + try { + const fileFlinger = new FileFlinger('myFlinger', { + pushers: [pusher], + // @ts-expect-error - Testing the error + watchPath: 2, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe('FileFlinger must have a valid watch path'); + } + }); + it(`Should throw an error if no pushers are passed`, () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileFlinger('myFlinger', { pushers: [] }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe('FileFlinger must have at least one pusher'); + } + }); + it(`Should emit errors if watcher or engine emit an error`, async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + const fileFlinger = new FileFlinger('myFlinger', { + watchPath: `${__dirname}/FileFlinger.test.ts`, + pushers: [pusher], + }); + await fileFlinger.start(); + let errorCount = 0; + fileFlinger.on('error', error => { + errorCount++; + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('My error'); + }); + // @ts-expect-error - Mocking the watcher + fileFlinger.watcher.emit('error', new Error('My error')); + // @ts-expect-error - Mocking the engine + fileFlinger.engine.emit('error', new Error('My error')); + expect(errorCount).toBe(2); + }); + }); +}); diff --git a/packages/api/file-flinger/src/FileFlinger.ts b/packages/api/file-flinger/src/FileFlinger.ts new file mode 100644 index 00000000..4fe1886a --- /dev/null +++ b/packages/api/file-flinger/src/FileFlinger.ts @@ -0,0 +1,150 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, type Layer } from '@mdf.js/core'; +import { Crash, Multi } from '@mdf.js/crash'; +import { DebugLogger, SetContext, type LoggerInstance } from '@mdf.js/logger'; +import EventEmitter from 'events'; +import { merge } from 'lodash'; +import { Registry } from 'prom-client'; +import { v4 } from 'uuid'; +import { Engine } from './engine'; +import { Keygen } from './keygen'; +import { DEFAULT_FILE_FLINGER_OPTIONS, type FileFlingerOptions } from './types'; +import { Watcher } from './watcher'; + +export declare interface FileFlinger { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Multi | Error) => void): this; + /** + * Add a listener for the status event, emitted when the component status changes. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: Health.Status) => void): this; +} + +/** + * FileFlinger class + * Allows to create a file processing service that can be used to watch a folder for new files and + * send them to a destination (S3, FTP, ...) using pushers. + * The service can be configured with a pattern to match the file names and generate keys for the + * destination. + * Once a file is processed, it can be moved to an archive folder (zipped optionally) or deleted. + * As a @mdf.js service, it offer prometheus metrics, health checks, and logging. + */ +export class FileFlinger extends EventEmitter implements Layer.App.Service { + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** The component identifier */ + public readonly componentId: string = v4(); + /** The options to create the FileFlinger */ + private readonly options: FileFlingerOptions; + /** The key generator to generate keys for the files */ + private readonly keygen: Keygen; + /** The watcher instance */ + private readonly watcher: Watcher; + /** Engine instance */ + private readonly engine: Engine; + /** + * Create a new instance of the FileFlinger with the given options + * @param name - The name of the instance of the FileFlinger + * @param options - The options to create the FileFlinger + */ + constructor( + public readonly name: string, + options: FileFlingerOptions + ) { + super(); + this.options = merge({}, DEFAULT_FILE_FLINGER_OPTIONS, options); + // Stryker disable next-line all + this.logger = SetContext( + this.options?.logger || new DebugLogger(`mdf:fileFlinger:${this.name}`), + this.name, + this.componentId + ); + if (typeof this.options.watchPath !== 'string' && !Array.isArray(this.options.watchPath)) { + throw new Crash(`FileFlinger must have a valid watch path`, this.componentId); + } + this.watcher = new Watcher( + { ...this.options, componentId: this.componentId, name: this.name }, + this.logger + ); + this.keygen = new Keygen(this.options, this.logger); + this.engine = new Engine( + this.keygen, + { ...this.options, componentId: this.componentId, name: this.name }, + this.logger + ); + } + /** Add event handler for new files */ + private readonly onAddEventHandler = (path: string) => { + // Stryker disable next-line all + this.logger.info(`New file detected: ${path}`); + this.engine.processFile(path); + }; + /** Pusher/Watcher event handlers */ + private readonly onErrorEventHandler = (error: Error | Crash) => { + // Stryker disable next-line all + this.logger.error(`${error.message}`); + if (this.listenerCount('error') > 0) { + this.emit('error', error); + } + }; + /** Perform the subscription to the events from pushers and watchers */ + private wrappingEvents(): void { + this.watcher.on('error', this.onErrorEventHandler); + this.watcher.on('add', this.onAddEventHandler); + this.engine.on('error', this.onErrorEventHandler); + } + /** Perform the unsubscription to the events from pushers and watchers */ + private unwrappingEvents(): void { + this.watcher.off('error', this.onErrorEventHandler); + this.watcher.off('add', this.onAddEventHandler); + this.engine.off('error', this.onErrorEventHandler); + } + /** Start the file flinger */ + public async start(): Promise { + this.wrappingEvents(); + await this.engine.start(); + await this.watcher.start(); + } + /** Stop the file flinger */ + public async stop(): Promise { + this.unwrappingEvents(); + await this.watcher.stop(); + await this.engine.stop(); + } + /** Close the file flinger */ + public async close(): Promise { + await this.stop(); + await this.watcher.close(); + await this.engine.close(); + } + /** Overall component status */ + public get status(): Health.Status { + return Health.overallStatus(this.checks); + } + /** + * Return the status of the file-flinger in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + return { ...this.watcher.checks, ...this.engine.checks }; + } + /** Return the metrics registry */ + public get metrics(): Registry { + return this.engine.metrics; + } +} diff --git a/packages/api/file-flinger/src/engine/Engine.test.ts b/packages/api/file-flinger/src/engine/Engine.test.ts new file mode 100644 index 00000000..be4cfb7a --- /dev/null +++ b/packages/api/file-flinger/src/engine/Engine.test.ts @@ -0,0 +1,215 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { v4 } from 'uuid'; +import { Keygen } from '../keygen'; +import { Pusher } from '../pusher'; +import { Engine } from './Engine'; + +// @ts-expect-error - Mocking the pusher +const pusher = { + name: 'test', + componentId: v4(), + push: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + close: jest.fn(), +} as Pusher; + +describe('#FileFlinger #Engine', () => { + describe('#Happy path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should create an instance of Engine', () => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { pushers: [pusher], componentId, name }); + expect(engine).toBeInstanceOf(Engine); + expect(engine.name).toEqual(name); + expect(engine.componentId).toEqual(componentId); + expect(engine.status).toEqual('pass'); + expect(engine.checks).toEqual({ + 'test:fileTasks': [ + { + componentId: componentId, + componentType: 'fileTasks', + observedUnit: 'errored files', + observedValue: [], + output: undefined, + status: 'pass', + time: expect.any(String), + }, + ], + 'test:lastOperation': [ + { + componentId: expect.any(String), + componentType: 'plug', + observedUnit: 'result of last operation', + observedValue: 'ok', + output: undefined, + status: 'pass', + time: undefined, + }, + ], + }); + expect(engine.metrics).toBeDefined(); + }, 300); + it('Should start, stop and close the engine', async () => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { pushers: [pusher], componentId, name }); + await engine.start(); + expect(pusher.start).toHaveBeenCalledTimes(1); + await engine.stop(); + expect(pusher.stop).toHaveBeenCalledTimes(1); + await engine.close(); + expect(pusher.close).toHaveBeenCalledTimes(1); + }, 300); + it(`Should process a file`, async () => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { pushers: [pusher], componentId, name }); + await engine.start(); + // @ts-expect-error - Mocking the limiter + jest.spyOn(engine.limiter, 'schedule').mockReturnValue('file'); + await engine.processFile('file'); + await engine.processFile('file'); + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(1); + // @ts-expect-error - Mocking the limiter + expect(engine.pendingProcess.has('file')).toBeTruthy(); + // @ts-expect-error - Mocking the limiter + engine.limiter.emit('done', 'file', {}, { taskId: 'file' }, null); + // @ts-expect-error - Mocking the limiter + expect(engine.pendingProcess.has('file')).toBeFalsy(); + }, 300); + }); + describe('#Sad path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it(`Should stop the started pushers if one of then fails to start and rejects`, async () => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { pushers: [pusher, pusher], componentId, name }); + jest + .spyOn(pusher, 'start') + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('Error starting pusher')); + await expect(engine.start()).rejects.toThrow('Error starting pusher'); + expect(pusher.start).toHaveBeenCalledTimes(2); + expect(pusher.stop).toHaveBeenCalledTimes(1); + }, 300); + it(`Should emit an error event if the keygen fails to generate a key and schedule an errored file task`, done => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { pushers: [pusher], componentId, name }); + engine.on('error', error => { + expect(error).toBeInstanceOf(Crash); + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(1); + // @ts-expect-error - Mocking the limiter + expect(engine.fileTasks.getProcessErroredFileTask).toHaveBeenCalledTimes(1); + done(); + }); + // @ts-expect-error - Mocking the limiter + jest.spyOn(keygen, 'generateKey').mockRejectedValue(new Error('Error generating key')); + // @ts-expect-error - Mocking the limiter + jest.spyOn(engine.limiter, 'schedule').mockReturnValue('file'); + // @ts-expect-error - Mocking the limiter + jest.spyOn(engine.fileTasks, 'getProcessErroredFileTask').mockReturnValue('task'); + engine.processFile('file'); + }, 300); + it(`Should emit an error event if the processFileTask fails, and schedule a task process again if task is present in the map`, done => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { + pushers: [pusher], + componentId, + name, + failedOperationDelay: 10, + }); + engine.start().then(); + engine.on('error', error => { + expect(error).toBeInstanceOf(Crash); + setTimeout(() => { + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(2); + done(); + }, 20); + }); + // @ts-expect-error - Mocking the limiter + jest.spyOn(engine.limiter, 'schedule').mockReturnValue('file'); + engine.processFile('file').then(() => { + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(1); + // @ts-expect-error - Mocking the limiter + expect(engine.pendingProcess.has('file')).toBeTruthy(); + // @ts-expect-error - Mocking the limiter + engine.limiter.emit( + 'done', + 'file', + {}, + { taskId: 'file' }, + new Crash('Error processing file') + ); + }); + }, 300); + it(`Should emit an error event if the processFileTask fails, and do not schedule a task process again if task is not present in the map`, done => { + const keygen = new Keygen(); + const componentId = v4(); + const name = 'test'; + const engine = new Engine(keygen, { + pushers: [pusher], + componentId, + name, + failedOperationDelay: 10, + }); + engine.start().then(); + engine.on('error', error => { + expect(error).toBeInstanceOf(Crash); + setTimeout(() => { + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(1); + done(); + }, 20); + }); + // @ts-expect-error - Mocking the limiter + jest.spyOn(engine.limiter, 'schedule').mockReturnValue('file'); + engine.processFile('file').then(() => { + // @ts-expect-error - Mocking the limiter + expect(engine.limiter.schedule).toHaveBeenCalledTimes(1); + // @ts-expect-error - Mocking the limiter + expect(engine.pendingProcess.has('file')).toBeTruthy(); + // @ts-expect-error - Mocking the limiter + engine.pendingProcess.delete('file'); + // @ts-expect-error - Mocking the limiter + engine.limiter.emit( + 'done', + 'file', + {}, + { taskId: 'file' }, + new Crash('Error processing file') + ); + }); + }, 300); + }); +}); diff --git a/packages/api/file-flinger/src/engine/Engine.ts b/packages/api/file-flinger/src/engine/Engine.ts new file mode 100644 index 00000000..43b3094f --- /dev/null +++ b/packages/api/file-flinger/src/engine/Engine.ts @@ -0,0 +1,233 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, type Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { DebugLogger, SetContext, type LoggerInstance } from '@mdf.js/logger'; +import { DoneListener, Limiter, Sequence } from '@mdf.js/tasks'; +import EventEmitter from 'events'; +import { merge } from 'lodash'; +import type { Registry } from 'prom-client'; +import { v4 } from 'uuid'; +import type { Keygen } from '../keygen'; +import { MetricsHandler } from '../metrics'; +import { PusherWrapper, type Pusher } from '../pusher'; +import { FileTasks } from './FileTasks'; +import { DEFAULT_ENGINE_OPTIONS, DEFAULT_LIMITER_OPTIONS, type EngineOptions } from './types'; + +export class Engine extends EventEmitter implements Layer.App.Resource { + /** Engine options */ + private readonly options: EngineOptions; + /** Pusher streams */ + private readonly pushers: Pusher[]; + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** Metrics handler */ + private readonly metricsHandler: MetricsHandler; + /** The limiter instance to manage concurrency*/ + private readonly limiter: Limiter; + /** File tasks */ + private readonly fileTasks: FileTasks; + /** Pending processes */ + private readonly pendingProcess: Map> = new Map(); + /** + * Create a new watcher instance with the given options + * @param keygen - The key generator to use + * @param options - The watcher options + * @param logger - The logger instance + */ + constructor( + private readonly keygen: Keygen, + options: EngineOptions, + logger?: LoggerInstance + ) { + super(); + this.options = merge({}, DEFAULT_ENGINE_OPTIONS, options); + // Stryker disable next-line all + this.logger = SetContext(logger || new DebugLogger(`mdf:fileFlinger:engine`), 'engine', v4()); + + this.pushers = this.options.pushers.map(pusher => { + return new PusherWrapper(pusher); + }); + this.limiter = new Limiter({ + ...DEFAULT_LIMITER_OPTIONS, + retryOptions: this.options.retryOptions ?? DEFAULT_LIMITER_OPTIONS.retryOptions, + }); + this.metricsHandler = new MetricsHandler(this.limiter); + this.fileTasks = new FileTasks(this.options, this.logger); + } + /** Pusher event handlers */ + private readonly onErrorEventHandler = (error: Error | Crash) => { + // Stryker disable next-line all + this.logger.error(`${error.message}`); + if (this.listenerCount('error') > 0) { + this.emit('error', error); + } + }; + /** Limiter done event handler */ + private readonly onDoneEventHandler: DoneListener = (uuid, result, meta, error) => { + this.logger.debug(`File processed: ${JSON.stringify(meta, null, 2)}`); + if (!error) { + this.logger.info(`Task [${uuid}] of type [${meta.taskId}] was completed successfully`); + if (this.pendingProcess.has(meta.taskId)) { + this.pendingProcess.delete(meta.taskId); + } + } else { + this.logger.error( + `Task [${uuid}] of type [${meta.taskId}] failed with error: ${error.message}` + ); + const task = this.pendingProcess.get(meta.taskId); + if (task) { + this.logger.debug(`Retrying task [${uuid}] of type [${meta.taskId}] ...`); + setTimeout( + this.limiter.schedule.bind(this.limiter), + this.options.failedOperationDelay, + task + ); + } else { + // Stryker disable next-line all + this.logger.error( + `Task [${uuid}] of type [${meta.taskId}] not found in the pending process` + ); + } + this.onErrorEventHandler(error); + } + }; + /** Perform the subscription to the events from pushers and limiter */ + private wrappingEvents(): void { + for (const pusher of this.pushers) { + pusher.on('error', this.onErrorEventHandler); + } + this.limiter.on('done', this.onDoneEventHandler); + } + /** Perform the unsubscription to the events from pushers and limiter */ + private unwrappingEvents(): void { + for (const pusher of this.pushers) { + pusher.off('error', this.onErrorEventHandler); + } + this.limiter.off('done', this.onDoneEventHandler); + } + /** Get the health of the file tasks handler */ + private get fileTasksHealth(): Health.Checks { + const status = this.fileTasks.erroredFiles.size > 0 ? Health.STATUS.FAIL : Health.STATUS.PASS; + return { + [`${this.name}:fileTasks`]: [ + { + status, + componentId: this.componentId, + componentType: 'fileTasks', + observedValue: Array.from(this.fileTasks.erroredFiles.values()), + observedUnit: 'errored files', + time: new Date().toISOString(), + output: status === Health.STATUS.FAIL ? 'Some files failed to be processed' : undefined, + }, + ], + }; + } + /** Add a file to the be processed */ + public async processFile(filePath: string): Promise { + let key: string; + try { + key = await this.keygen.generateKey(filePath); + if (this.pendingProcess.has(key)) { + this.logger.warn(`File [${filePath}] already in process, skipping ...`); + return; + } + } catch (rawError) { + this.limiter.schedule(this.fileTasks.getProcessErroredFileTask(filePath, rawError)); + this.onErrorEventHandler(Crash.from(rawError, this.componentId)); + return; + } + const task = this.fileTasks.getProcessFileTask(filePath, key); + this.pendingProcess.set(key, task); + this.limiter.schedule(task); + this.logger.info(`Task for [${key}] was scheduled`); + } + /** Start the file flinger */ + public async start(): Promise { + this.wrappingEvents(); + const startedPushers = []; + try { + for (const pusher of this.pushers) { + // Stryker disable next-line all + this.logger.info(`Starting pusher ${pusher.name} ...`); + await pusher.start(); + // Stryker disable next-line all + this.logger.info(`... pusher ${pusher.name} started`); + startedPushers.push(pusher); + } + this.limiter.start(); + } catch (rawError) { + const error = Crash.from(rawError, this.componentId); + // Stryker disable next-line all + this.logger.error(`Error starting the file flinger: ${error.message}`); + // Stryker disable next-line all + this.logger.warn(`Stopping all the already started pushers ...`); + for (const pusher of startedPushers) { + // Stryker disable next-line all + this.logger.info(`Stopping pusher ${pusher.name} ...`); + await pusher.stop(); + // Stryker disable next-line all + this.logger.info(`... pusher ${pusher.name} stopped`); + } + throw error; + } + } + /** Stop the file flinger */ + public async stop(): Promise { + this.unwrappingEvents(); + this.limiter.stop(); + for (const pusher of this.pushers) { + // Stryker disable next-line all + this.logger.info(`Stopping pusher ${pusher.name} ...`); + await pusher.stop(); + // Stryker disable next-line all + this.logger.info(`... pusher ${pusher.name} stopped`); + } + } + /** Close the file flinger */ + public async close(): Promise { + await this.stop(); + for (const pusher of this.pushers) { + // Stryker disable next-line all + this.logger.info(`Closing pusher ${pusher.name} ...`); + await pusher.close(); + // Stryker disable next-line all + this.logger.info(`... pusher ${pusher.name} closed`); + } + this.metricsHandler.registry.clear(); + this.limiter.clear(); + } + /** Get the name of the watcher */ + public get name(): string { + return this.options.name; + } + /** Get the component identifier */ + public get componentId(): string { + return this.options.componentId; + } + /** Overall component status */ + public get status(): Health.Status { + return Health.overallStatus(this.checks); + } + /** + * Return the status of the file-flinger in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + let overallChecks: Health.Checks = {}; + for (const pusher of this.pushers) { + overallChecks = { ...pusher.checks, ...overallChecks }; + } + return { ...overallChecks, ...this.fileTasksHealth }; + } + /** Return the metrics registry */ + public get metrics(): Registry { + return this.metricsHandler.registry; + } +} diff --git a/packages/api/file-flinger/src/engine/FileTasks.test.ts b/packages/api/file-flinger/src/engine/FileTasks.test.ts new file mode 100644 index 00000000..a4f56b98 --- /dev/null +++ b/packages/api/file-flinger/src/engine/FileTasks.test.ts @@ -0,0 +1,633 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { Sequence, Single } from '@mdf.js/tasks'; +import child_process from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import stream from 'stream'; +import { v4 } from 'uuid'; +import { Pusher } from '../pusher'; +import { FileTasks } from './FileTasks'; +import { ErrorStrategy, PostProcessingStrategy } from './types'; + +// @ts-expect-error - Mocking the pusher +const pusher = { + name: 'test', + componentId: v4(), + push: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + close: jest.fn(), +} as Pusher; +describe('#FileFlinger #FileTasks', () => { + describe('#Happy path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should create a new instance of FileTasks', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + const fileFlinger = new FileTasks({ pushers: [pusher] }); + expect(fileFlinger).toBeInstanceOf(FileTasks); + }); + it('Should create a task handler as a sequence for file processing with default values', () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ pushers: [pusher] }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + // Verify that the sequence is an instance of Sequence + expect(sequence).toBeInstanceOf(Sequence); + }); + it('Should create a task handler as a sequence for file processing with "delete" as post processing task', () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + // Verify that the sequence is an instance of Sequence + expect(sequence).toBeInstanceOf(Sequence); + //@ts-expect-error - Verify that the post processing task is a sequence + expect(sequence.pattern.post[0].taskId).toEqual('delete'); + }); + it('Should create a task handler as a sequence for file processing with "archive" as post processing task', () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ARCHIVE, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + // Verify that the sequence is an instance of Sequence + expect(sequence).toBeInstanceOf(Sequence); + //@ts-expect-error - Verify that the post processing task is a sequence + expect(sequence.pattern.post[0].taskId).toEqual('archive'); + }); + it('Should create a task handler as a sequence for file processing with "zip" as post processing task', () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ZIP, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + // Verify that the sequence is an instance of Sequence + expect(sequence).toBeInstanceOf(Sequence); + //@ts-expect-error - Verify that the post processing task is a sequence + expect(sequence.pattern.post[0].taskId).toEqual('zip'); + }); + it('Should create a task handler as a single for error processing with default values', () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ pushers: [pusher] }); + // Get the process directory task sequence + const sequence = fileTasks.getProcessErroredFileTask( + '/path/to/directory', + new Error('Test error') + ); + // Verify that the sequence is an instance of Sequence + expect(sequence).toBeInstanceOf(Single); + }); + it('Should process a file properly if all the tasks resolve with a "delete" post processing strategy in a linux system - error: { code: 1 } & !stderr', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeDefined(); + }); + it('Should process a file properly if all the tasks resolve with a "delete" post processing strategy in a linux system - stdout: ""', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, '', null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeDefined(); + }); + it('Should process a file properly if all the tasks resolve with a "delete" post processing strategy in a windows system - stdout: "True"', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'False', null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeDefined(); + }); + it(`Should process a file properly if all the tasks resolve with a "archive" post processing strategy in a linux system - error: { code: 1 } & !stderr`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ARCHIVE, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'renameSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeDefined(); + }); + it(`Should process a file properly if all the tasks resolve with a "zip" post processing strategy in a linux system - error: { code: 1 } & !stderr`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ZIP, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'createReadStream').mockReturnValue({} as any); + jest.spyOn(fs, 'createWriteStream').mockReturnValue({} as any); + jest.spyOn(stream, 'pipeline').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (a: any, b: any, c: any, cb: (error: any) => void) => { + cb(null); + } + ); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeDefined(); + }); + it(`Should process an errored file properly if all the tasks resolve with a "delete" error strategy in a linux system - error: { code: 1 } & !stderr`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + errorStrategy: ErrorStrategy.DELETE, + }); + // Get the process directory task sequence + const sequence = fileTasks.getProcessErroredFileTask( + '/path/to/directory', + new Error('Test error') + ); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeUndefined(); + const entries = Array.from(fileTasks.erroredFiles.entries()); + expect(entries).toHaveLength(1); + expect(entries[0][0]).toEqual('directory'); + expect(entries[0][1]).toEqual({ + errorTrace: ['Error: Test error'], + path: '/path/to/directory', + strategy: 'delete', + }); + }); + it(`Should process an errored file properly if all the tasks resolve with a "dead letter" error strategy in a linux system - error: { code: 1 } & !stderr`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + errorStrategy: ErrorStrategy.DEAD_LETTER, + deadLetterFolder: '/path/to/dead-letter', + }); + // Get the process directory task sequence + const sequence = fileTasks.getProcessErroredFileTask( + '/path/to/directory', + new Error('Test error') + ); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(fs, 'renameSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeUndefined(); + const entries = Array.from(fileTasks.erroredFiles.entries()); + expect(entries).toHaveLength(1); + expect(entries[0][0]).toEqual('directory'); + expect(entries[0][1]).toEqual({ + errorTrace: ['Error: Test error'], + path: '/path/to/directory', + strategy: 'dead-letter', + }); + }); + it(`Should process an errored file properly if all the tasks resolve with a "ignore" error strategy in a linux system - error: { code: 1 } & !stderr`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + errorStrategy: ErrorStrategy.IGNORE, + }); + // Get the process directory task sequence + const sequence = fileTasks.getProcessErroredFileTask( + '/path/to/directory', + new Error('Test error') + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeUndefined(); + const entries = Array.from(fileTasks.erroredFiles.entries()); + expect(entries).toHaveLength(1); + expect(entries[0][0]).toEqual('directory'); + expect(entries[0][1]).toEqual({ + errorTrace: ['Error: Test error'], + path: '/path/to/directory', + strategy: 'ignore', + }); + }); + it(`Should process an errored file properly if delete process fail with a "delete" error strategy in a linux system - error: { code: 1 } & !stderr including it in the register`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + errorStrategy: ErrorStrategy.DELETE, + }); + // Get the process directory task sequence + const sequence = fileTasks.getProcessErroredFileTask( + '/path/to/directory', + new Error('Test error') + ); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'no-null', null); + } + ); + // Run the sequence + await expect(sequence.execute()).resolves.toBeUndefined(); + const entries = Array.from(fileTasks.erroredFiles.entries()); + expect(entries).toHaveLength(1); + expect(entries[0][0]).toEqual('directory'); + expect(entries[0][1]).toEqual({ + errorTrace: [ + 'Error: Test error', + 'CrashError: Error processing file [directory]: Error deleting file /path/to/directory: File [/path/to/directory] is currently open and cannot be deleted', + 'caused by CrashError: Error deleting file /path/to/directory: File [/path/to/directory] is currently open and cannot be deleted', + 'caused by CrashError: File [/path/to/directory] is currently open and cannot be deleted', + ], + path: '/path/to/directory', + strategy: 'delete', + }); + }); + }); + describe('#Sad path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it(`Should throw an error if no pushers are passed`, () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileTasks({ pushers: [] }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe('FileFlinger must have at least one pusher'); + } + }); + it(`Should throw an error if the post processing strategy is not to delete and there is not an archive folder`, () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ARCHIVE, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe( + 'FileFlinger must have an archive folder if postProcessingStrategy is not DELETE' + ); + } + }); + it(`Should throw an error if the error strategy is dead letter and there is not a dead letter folder`, () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileTasks({ + pushers: [pusher], + errorStrategy: ErrorStrategy.DEAD_LETTER, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe( + 'FileFlinger must have a dead-letter folder if onKeyingError is DEAD_LETTER' + ); + } + }); + it('Should throw a error if the post processing strategy is supported', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileTasks({ + pushers: [pusher], + postProcessingStrategy: 'unsupported' as PostProcessingStrategy, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe('Unknown post-processing strategy: unsupported'); + } + }); + it('Should throw a error if the error strategy is supported', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + try { + new FileTasks({ + pushers: [pusher], + errorStrategy: 'unsupported' as ErrorStrategy, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Error).message).toBe('Unknown error strategy: unsupported'); + } + }); + it('Should rejects the file processing if there is a problem in the push file process', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockRejectedValue(new Error('myError')); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Execution error in task [push]: CrashError: Execution error in task [push]: Error pushing file /path/to/file.txt to test: myError,\ncaused by InterruptionError: Too much attempts [1], the promise will not be retried,\ncaused by CrashError: Error pushing file /path/to/file.txt to test: myError,\ncaused by Error: myError' + ); + }); + it('Should rejects the file processing if the file is in use with a "delete" post processing strategy in a linux system - stdout: "no-null"', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'no-null', null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [delete]: Error deleting file /path/to/file.txt: File [/path/to/file.txt] is currently open and cannot be deleted' + ); + }); + it('Should rejects the file processing if the file is in use with a "delete" post processing strategy in a linux system - stderr: "no-null"', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 2, message: 'myError' }, null, 'no-null'); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [delete]: Error deleting file /path/to/file.txt: Error checking file status with lsof: myError' + ); + }); + it('Should rejects the file processing if the file is in use with a "delete" post processing strategy in a windows system - error', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 2, message: 'myError' }, null, 'no-null'); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [delete]: Error deleting file /path/to/file.txt: Error checking file status with PowerShell: myError' + ); + }); + it('Should rejects the file processing if the file is in use with a "delete" post processing strategy in a windows system - stdout: "True"', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.DELETE, + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'unlinkSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'True', null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [delete]: Error deleting file /path/to/file.txt: File [/path/to/file.txt] is currently open and cannot be deleted' + ); + }); + it('Should rejects the file processing if the file is in use with a "archive" post processing strategy in a linux system - stdout: "no-null"', async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ARCHIVE, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'renameSync').mockReturnValue(); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'no-null', null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [archive]: Error archiving file [/path/to/file.txt] to [/path/to/archive]: File [/path/to/file.txt] is currently open and cannot be archived' + ); + }); + it(`Should rejects the file processing if the file is in use with a "zip" post processing strategy in a linux system - stdout: "no-null"`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ZIP, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'createReadStream').mockReturnValue({} as any); + jest.spyOn(fs, 'createWriteStream').mockReturnValue({} as any); + jest.spyOn(stream, 'pipeline').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (a: any, b: any, c: any, cb: (error: any) => void) => { + cb(null); + } + ); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb(null, 'no-null', null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [zip]: File [/path/to/file.txt] is currently open and cannot be zipped' + ); + }); + it(`Should rejects the file processing if the file can not be open with a "zip" post processing strategy`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ZIP, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'createReadStream').mockImplementation(() => { + throw new Error('myError'); + }); + jest.spyOn(fs, 'createWriteStream').mockReturnValue({} as any); + jest.spyOn(stream, 'pipeline').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (a: any, b: any, c: any, cb: (error: any) => void) => { + cb(null); + } + ); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [zip]: Error creating read/write streams: myError' + ); + }); + it(`Should rejects the file processing if there is a problem zipping the file with a "zip" post processing strategy`, async () => { + // Create a new FileTasks instance + const fileTasks = new FileTasks({ + pushers: [pusher], + postProcessingStrategy: PostProcessingStrategy.ZIP, + archiveFolder: '/path/to/archive', + }); + // Get the process file task sequence + const sequence = fileTasks.getProcessFileTask('/path/to/file.txt', 'file-key'); + jest.spyOn(os, 'type').mockReturnValue('Linux'); + jest.spyOn(pusher, 'push').mockResolvedValue(); + jest.spyOn(fs, 'createReadStream').mockReturnValue({} as any); + jest.spyOn(fs, 'createWriteStream').mockReturnValue({} as any); + jest.spyOn(stream, 'pipeline').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (a: any, b: any, c: any, cb: (error: any) => void) => { + cb(new Error('myError')); + } + ); + jest.spyOn(child_process, 'execFile').mockImplementation( + // @ts-expect-error - Mocking the execFileSync + (file: string, args: string[], cb: (error: any, stdout: any, stderr: any) => void) => { + cb({ code: 1 }, null, null); + } + ); + // Run the sequence + await expect(sequence.execute()).rejects.toThrow( + 'Execution error in task [file-key]: Error executing the [post] phase: Execution error in task [zip]: Error zipping file [/path/to/file.txt]: myError' + ); + }); + }); +}); diff --git a/packages/api/file-flinger/src/engine/FileTasks.ts b/packages/api/file-flinger/src/engine/FileTasks.ts new file mode 100644 index 00000000..c3c39eb0 --- /dev/null +++ b/packages/api/file-flinger/src/engine/FileTasks.ts @@ -0,0 +1,423 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { DebugLogger, SetContext, type LoggerInstance } from '@mdf.js/logger'; +import { Group, RETRY_STRATEGY, Sequence, Single } from '@mdf.js/tasks'; +import { execFile, type ExecFileException } from 'child_process'; +import { + createReadStream, + createWriteStream, + ReadStream, + renameSync, + unlinkSync, + WriteStream, +} from 'fs'; +import { merge } from 'lodash'; +import os from 'os'; +import path from 'path'; +import { pipeline } from 'stream'; +import { v4 } from 'uuid'; +import { createGzip } from 'zlib'; +import type { Pusher } from '../pusher'; +import { + DEFAULT_FILE_TASKS_OPTIONS, + ERROR_STRATEGIES, + ErroredFile, + ErrorStrategy, + FileTaskIdentifiers, + POST_PROCESSING_STRATEGIES, + PostProcessingStrategy, + type FileTasksOptions, +} from './types'; + +/** File tasks to process files */ +export class FileTasks { + /** File task options */ + private readonly options: FileTasksOptions; + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** Error strategy */ + private readonly errorStrategy: ErrorStrategy; + /** Post-processing strategy */ + private readonly postProcessingStrategy: PostProcessingStrategy; + /** Errored file tracks */ + public readonly erroredFiles: Map = new Map(); + /** + * Create a new instance of the file tasks. + * @param options - The file task options + */ + constructor(options: FileTasksOptions, logger?: LoggerInstance) { + this.options = merge({}, DEFAULT_FILE_TASKS_OPTIONS, options); + // Stryker disable next-line all + this.logger = SetContext( + logger || new DebugLogger(`mdf:fileFlinger:engine:fileTasks`), + 'fileTasks', + v4() + ); + this.errorStrategy = this.options.errorStrategy ?? ErrorStrategy.IGNORE; + this.postProcessingStrategy = + this.options.postProcessingStrategy ?? PostProcessingStrategy.DELETE; + if (this.options.pushers.length < 1) { + throw new Crash(`FileFlinger must have at least one pusher`); + } + if (!POST_PROCESSING_STRATEGIES.includes(this.postProcessingStrategy)) { + throw new Crash(`Unknown post-processing strategy: ${this.postProcessingStrategy}`); + } + if ( + this.postProcessingStrategy !== PostProcessingStrategy.DELETE && + !this.options.archiveFolder + ) { + throw new Crash( + `FileFlinger must have an archive folder if postProcessingStrategy is not DELETE` + ); + } + if (!ERROR_STRATEGIES.includes(this.errorStrategy)) { + throw new Crash(`Unknown error strategy: ${this.errorStrategy}`); + } + if (this.errorStrategy === ErrorStrategy.DEAD_LETTER && !this.options.deadLetterFolder) { + throw new Crash(`FileFlinger must have a dead-letter folder if onKeyingError is DEAD_LETTER`); + } + } + /** + * Get the task to process the file. + * @param filePath - The path of the file to process + * @param key - The key to use for the file + * @returns The task to process the file + */ + public getProcessFileTask(filePath: string, key: string): Sequence { + this.logger.debug( + `Processing file [${filePath}] with key [${key}] using [${this.options.pushers.map(p => p.name).join(', ')}]` + ); + return new Sequence( + { task: this.getPushTasks(filePath, key), post: [this.selectPostProcessTask(filePath)] }, + { + bind: this, + id: key, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + retryStrategy: RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS, + } + ); + } + /** + * Get the task to process the errored file based on the error strategy. + * @param filePath - The path of the file to process + * @param error - The error to process + * @returns The task to process the errored file + */ + public getProcessErroredFileTask(filePath: string, error: unknown): Single { + this.logger.debug(`Processing errored file [${filePath}}]`); + return new Single(this.processErroredFile, [filePath, error], { + id: FileTaskIdentifiers.ERRORED, + bind: this, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + }); + } + /** + * Select the post-process task based on the options. + * @param filePath - The path of the file to process + * @returns The post-process task + */ + private selectPostProcessTask(filePath: string): Single { + this.logger.debug( + `Selecting post-processing task for file [${filePath}], strategy: [${this.postProcessingStrategy}]` + ); + switch (this.postProcessingStrategy) { + case PostProcessingStrategy.ARCHIVE: + return this.getArchiveTask(filePath, this.options.archiveFolder as string); + case PostProcessingStrategy.ZIP: + return this.getZipTask(filePath, this.options.archiveFolder as string); + case PostProcessingStrategy.DELETE: + default: + return this.getDeleteTask(filePath); + } + } + /** + * Get the group of tasks to process the file. + * @param filePath - The path of the file to process + * @param key - The key to use for the file + * @returns The group of tasks to process the file + */ + private getPushTasks(filePath: string, key: string): Group { + this.logger.debug(`Getting push tasks for file [${filePath}] with key [${key}]`); + const pushTasks: Single[] = []; + for (const pusher of this.options.pushers) { + pushTasks.push( + new Single(this.push, [filePath, key, pusher], { + id: FileTaskIdentifiers.PUSH, + bind: this, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + retryStrategy: RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS, + }) + ); + } + return new Group(pushTasks, { + id: 'push', + retryOptions: { attempts: 1 }, + retryStrategy: RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS, + }); + } + /** + * Get the task to archive the file to the destination folder. + * @param filePath - The path of the file to archive + * @param destination - The destination folder path + * @returns The task to archive the file + */ + private getArchiveTask(filePath: string, destination: string): Single { + this.logger.debug(`Getting archive task for file [${filePath}] to [${destination}]`); + return new Single(this.archiveFile, [filePath, destination], { + id: FileTaskIdentifiers.ARCHIVE, + bind: this, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + }); + } + /** + * Get the task to zip the file and move it to the destination folder. + * Deletes the original file after zipping. + * @param filePath - The path of the file to zip + * @param destination - The destination folder path + * @returns The task to zip the file + */ + private getZipTask(filePath: string, destination: string): Single { + this.logger.debug(`Getting zip task for file [${filePath}] to [${destination}]`); + return new Single(this.zipFile, [filePath, destination], { + id: FileTaskIdentifiers.ZIP, + bind: this, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + }); + } + /** + * Get the task to delete the file from the file system. + * @param filePath - The path of the file to delete + * @returns The task to delete the file + */ + private getDeleteTask(filePath: string): Single { + this.logger.debug(`Getting delete task for file [${filePath}]`); + return new Single(this.deleteFile, [filePath], { + id: FileTaskIdentifiers.DELETE, + bind: this, + retryOptions: { ...this.options.retryOptions, attempts: 1 }, + }); + } + /** + * Push the file to the pusher and monitor the process. + * @param filePath - The path of the file to process + * @param key - The key to use for the file + * @param pusher - The pusher to use + */ + private async push(filePath: string, key: string, pusher: Pusher): Promise { + this.logger.debug(`Pushing file [${filePath}] to [${pusher.name}] with key [${key}]`); + try { + await pusher.push(filePath, key); + } catch (rawError) { + const cause = Crash.from(rawError, pusher.componentId); + const error = new Crash( + `Error pushing file ${filePath} to ${pusher.name}: ${cause.message}`, + { cause } + ); + this.logger.debug(error.message); + throw error; + } + } + /** + * Delete the file from the file system. + * @param filePath - The path of the file to process + */ + private async deleteFile(filePath: string): Promise { + try { + if (await this.isFileOpen(filePath)) { + const error = new Crash(`File [${filePath}] is currently open and cannot be deleted`); + this.logger.debug(error.message); + throw error; + } + unlinkSync(filePath); + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error deleting file ${filePath}: ${cause.message}`, { cause }); + this.logger.debug(error.message); + throw error; + } + } + /** + * Archive the file to the destination folder. + * @param filePath - The path of the file to archive + * @param destination - The destination folder path + */ + private async archiveFile(filePath: string, destination: string): Promise { + try { + if (await this.isFileOpen(filePath)) { + const error = new Crash(`File [${filePath}] is currently open and cannot be archived`); + this.logger.debug(error.message); + throw error; + } + renameSync(filePath, path.join(destination, path.basename(filePath))); + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash( + `Error archiving file [${filePath}] to [${destination}]: ${cause.message}`, + { cause } + ); + this.logger.debug(error.message); + throw error; + } + } + /** + * Zips the file and moves it to the destination folder. + * Deletes the original file after zipping. + * @param filePath - The path of the file to zip + * @param destination - The destination folder path + */ + private async zipFile(filePath: string, destination: string): Promise { + if (await this.isFileOpen(filePath)) { + const error = new Crash(`File [${filePath}] is currently open and cannot be zipped`); + this.logger.debug(error.message); + throw error; + } + return new Promise((resolve, reject) => { + const zipFilePath = path.join(destination, `${path.basename(filePath)}.gz`); + let readStream: ReadStream; + let writeStream: WriteStream; + try { + readStream = createReadStream(filePath, 'utf8'); + writeStream = createWriteStream(zipFilePath); + } catch (streamError) { + const cause = Crash.from(streamError); + const error = new Crash(`Error creating read/write streams: ${cause.message}`, { cause }); + this.logger.debug(error.message); + reject(error); + return; + } + const gZip = createGzip(); + pipeline(readStream, gZip, writeStream, rawError => { + if (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error zipping file [${filePath}]: ${cause.message}`, { cause }); + this.logger.debug(error.message); + reject(error); + } + resolve(); + }); + }); + } + /** + * Process the errored file based on the error strategy. + * @param filePath - The path of the file to process + */ + private async processErroredFile(filePath: string, error: unknown): Promise { + const filename = path.basename(filePath, path.extname(filePath)); + const errorTrack = new Multi(`Error processing file ${filename}`, { + causes: [Crash.from(error)], + }); + this.logger.debug(`Processing errored file [${filename}] with strategy: ${this.errorStrategy}`); + try { + switch (this.errorStrategy) { + case ErrorStrategy.DELETE: + await this.deleteFile(filePath); + break; + case ErrorStrategy.DEAD_LETTER: + if (!this.options.deadLetterFolder) { + throw new Crash(`Dead-letter strategy selected but no dead-letter folder provided`); + } + await this.archiveFile(filePath, this.options.deadLetterFolder); + break; + case ErrorStrategy.IGNORE: + default: + break; + } + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error processing file [${filename}]: ${cause.message}`, { cause }); + this.logger.debug(error.message); + errorTrack.push(error); + } + this.erroredFiles.set(filename, { + errorTrace: errorTrack.trace(), + path: filePath, + strategy: this.errorStrategy, + }); + } + /** + * Detect if a file is currently open by any process. + * Compatible with Unix and Windows systems. + * @param filePath - The absolute path to the file. + * @returns A promise that resolves to `true` if the file is open, or `false` otherwise. + */ + private async isFileOpen(filePath: string): Promise { + this.logger.debug(`Checking if file [${filePath}] is open`); + // Specific implementation according to the operating system + if (os.type() === 'Windows_NT') { + return this.isFileOpenWindows(filePath); + } else { + return this.isFileOpenUnix(filePath); + } + } + /** + * Check if a file is open in Unix systems using the 'lsof' command. + * @param filePath - The absolute path to the file. + * @returns A promise that resolves to `true` if the file is open, or `false` otherwise. + */ + private async isFileOpenUnix(filePath: string): Promise { + this.logger.debug(`Checking if file [${filePath}] is open in Unix systems`); + return new Promise((resolve, reject) => { + execFile( + 'lsof', + ['-F', 'n', '--', filePath], + (error: ExecFileException | null, stdout: string, stderr: string) => { + if (error) { + // If 'lsof' exits with code 1 and no stderr, the file is not open + if (error.code === 1 && !stderr) { + resolve(false); + } + // Otherwise, an error occurred + else { + reject(new Crash(`Error checking file status with lsof: ${error.message}`)); + } + } else { + // If 'lsof' returns output, the file is open + resolve(stdout.trim().length > 0); + } + } + ); + }); + } + /** + * Check if a file is open in Windows using PowerShell commands. + * @param filePath - The absolute path to the file. + * @returns A promise that resolves to `true` if the file is open, or `false` otherwise. + */ + private async isFileOpenWindows(filePath: string): Promise { + const powershellScript = ` + $filePath = "${filePath.replace(/"/g, '""')}"; + $file = Get-Item -LiteralPath $filePath -ErrorAction SilentlyContinue; + if ($file -ne $null) { + try { + $stream = $file.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::None); + $stream.Close(); + Write-Output "False"; + } catch { + Write-Output "True"; + } + } else { + Write-Output "False"; + } + `; + return new Promise((resolve, reject) => { + // PowerShell script to check if the file is open + execFile( + 'powershell.exe', + ['-NoProfile', '-Command', powershellScript], + (error: ExecFileException | null, stdout: string, stderr: string) => { + if (error) { + reject(new Crash(`Error checking file status with PowerShell: ${error.message}`)); + } else { + resolve(stdout.trim() === 'True'); + } + } + ); + }); + } +} diff --git a/packages/api/file-flinger/src/engine/index.ts b/packages/api/file-flinger/src/engine/index.ts new file mode 100644 index 00000000..68b62e7e --- /dev/null +++ b/packages/api/file-flinger/src/engine/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Engine'; +export * from './types'; diff --git a/packages/api/file-flinger/src/engine/types/EngineOptions.i.ts b/packages/api/file-flinger/src/engine/types/EngineOptions.i.ts new file mode 100644 index 00000000..1cd73d50 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/EngineOptions.i.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { FileTasksOptions } from './FileTasksOptions.i'; + +/** Engine options */ +export interface EngineOptions extends FileTasksOptions { + /** The name of the watcher */ + name: string; + /** The component identifier */ + componentId: string; + /** Delay between retries failed file processing operations */ + failedOperationDelay?: number; +} diff --git a/packages/api/file-flinger/src/engine/types/ErrorStrategy.t..ts b/packages/api/file-flinger/src/engine/types/ErrorStrategy.t..ts new file mode 100644 index 00000000..a2caf3e7 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/ErrorStrategy.t..ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Error strategy */ +export enum ErrorStrategy { + /** Ignore the file, emit an error and include in the skipped files */ + IGNORE = 'ignore', + /** Delete the file, emit an error and include in the skipped files */ + DELETE = 'delete', + /** Move the file to the dead-letter folder, emit an error and include in the skipped files */ + DEAD_LETTER = 'dead-letter', +} + +/** List of error strategies */ +export const ERROR_STRATEGIES: ErrorStrategy[] = [ + ErrorStrategy.IGNORE, + ErrorStrategy.DELETE, + ErrorStrategy.DEAD_LETTER, +]; diff --git a/packages/api/file-flinger/src/engine/types/ErroredFile.i.ts b/packages/api/file-flinger/src/engine/types/ErroredFile.i.ts new file mode 100644 index 00000000..42f7b4c3 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/ErroredFile.i.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { ErrorStrategy } from './ErrorStrategy.t.'; + +/** The ErroredFile interface */ +export interface ErroredFile { + /** The file path */ + path: string; + /** The error message */ + errorTrace: string[]; + /** The error strategy to applied */ + strategy: ErrorStrategy; +} diff --git a/packages/api/file-flinger/src/engine/types/FileTaskIdentifiers.t.ts b/packages/api/file-flinger/src/engine/types/FileTaskIdentifiers.t.ts new file mode 100644 index 00000000..7dbe148d --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/FileTaskIdentifiers.t.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** File task identifiers */ +export enum FileTaskIdentifiers { + ERRORED = 'errored', + PUSH = 'push', + DELETE = 'delete', + ARCHIVE = 'archive', + ZIP = 'zip', +} diff --git a/packages/api/file-flinger/src/engine/types/FileTasksOptions.i.ts b/packages/api/file-flinger/src/engine/types/FileTasksOptions.i.ts new file mode 100644 index 00000000..f64c3e04 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/FileTasksOptions.i.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { RetryOptions } from '@mdf.js/utils'; +import type { Pusher } from '../../pusher'; +import type { ErrorStrategy } from './ErrorStrategy.t.'; +import { PostProcessingStrategy } from './PostProcessingStrategy.t'; + +/** File tasks options */ +export interface FileTasksOptions { + /** Retry options for file operations */ + retryOptions?: RetryOptions; + /** Pushers to send the files to */ + pushers: Pusher[]; + /** Archive folder for processed files */ + archiveFolder?: string; + /** Dead-letter folder for files with keying errors */ + deadLetterFolder?: string; + /** Determine the post-processing strategy for files without errors */ + postProcessingStrategy?: PostProcessingStrategy; + /** Determine the error strategy for files with errors */ + errorStrategy?: ErrorStrategy; +} diff --git a/packages/api/file-flinger/src/engine/types/PostProcessingStrategy.t.ts b/packages/api/file-flinger/src/engine/types/PostProcessingStrategy.t.ts new file mode 100644 index 00000000..d3b94c78 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/PostProcessingStrategy.t.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Post-processing strategies for errored files */ +export enum PostProcessingStrategy { + /** Archive the file */ + ARCHIVE = 'archive', + /** Delete the file */ + DELETE = 'delete', + /** Zip the file */ + ZIP = 'zip', +} + +/** List of post-processing strategies */ +export const POST_PROCESSING_STRATEGIES: PostProcessingStrategy[] = [ + PostProcessingStrategy.ARCHIVE, + PostProcessingStrategy.DELETE, + PostProcessingStrategy.ZIP, +]; diff --git a/packages/api/file-flinger/src/engine/types/const.ts b/packages/api/file-flinger/src/engine/types/const.ts new file mode 100644 index 00000000..43624316 --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/const.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { LimiterOptions } from '@mdf.js/tasks'; +import { v4 } from 'uuid'; +import type { EngineOptions } from './EngineOptions.i'; +import { ErrorStrategy } from './ErrorStrategy.t.'; +import type { FileTasksOptions } from './FileTasksOptions.i'; +import { PostProcessingStrategy } from './PostProcessingStrategy.t'; + +/** Default limiter options */ +export const DEFAULT_LIMITER_OPTIONS: LimiterOptions = { + concurrency: 1, + autoStart: true, + delay: 1000, + retryOptions: { + attempts: 3, + maxWaitTime: 60000, + timeout: 10000, + waitTime: 1000, + }, +}; + +/** Default file tasks options */ +export const DEFAULT_FILE_TASKS_OPTIONS: FileTasksOptions = { + retryOptions: DEFAULT_LIMITER_OPTIONS.retryOptions, + pushers: [], + archiveFolder: undefined, + deadLetterFolder: undefined, + postProcessingStrategy: PostProcessingStrategy.DELETE, + errorStrategy: ErrorStrategy.IGNORE, +}; + +/** Default engine options */ +export const DEFAULT_ENGINE_OPTIONS: EngineOptions = { + name: 'engine', + componentId: v4(), + failedOperationDelay: 30000, + ...DEFAULT_FILE_TASKS_OPTIONS, +}; diff --git a/packages/api/file-flinger/src/engine/types/index.ts b/packages/api/file-flinger/src/engine/types/index.ts new file mode 100644 index 00000000..f9b195ad --- /dev/null +++ b/packages/api/file-flinger/src/engine/types/index.ts @@ -0,0 +1,14 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './const'; +export * from './EngineOptions.i'; +export * from './ErroredFile.i'; +export * from './ErrorStrategy.t.'; +export * from './FileTaskIdentifiers.t'; +export * from './FileTasksOptions.i'; +export * from './PostProcessingStrategy.t'; diff --git a/packages/api/file-flinger/src/index.ts b/packages/api/file-flinger/src/index.ts new file mode 100644 index 00000000..a5d4267a --- /dev/null +++ b/packages/api/file-flinger/src/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Health, Jobs } from '@mdf.js/core'; +export { LoggerInstance } from '@mdf.js/logger'; +export { RetryOptions } from '@mdf.js/utils'; + +export { ErrorStrategy, PostProcessingStrategy } from './engine'; +export * from './FileFlinger'; +export type { Pusher } from './pusher'; +export type { FileFlingerOptions } from './types'; diff --git a/packages/api/file-flinger/src/keygen/Keygen.test.ts b/packages/api/file-flinger/src/keygen/Keygen.test.ts new file mode 100644 index 00000000..369c5337 --- /dev/null +++ b/packages/api/file-flinger/src/keygen/Keygen.test.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Keygen } from './Keygen'; +import type { KeygenOptions } from './types'; // Adjust the import path as necessary + +describe('#FileFlinger #Keygen', () => { + describe('#Happy path', () => { + it('Should generate key with default patterns and values', () => { + // Initialize Keygen with default options + const keygen = new Keygen(); + // Generate key for a sample file + const key = keygen.generateKey('/path/to/myfile.txt'); + // Expect the key to be the filename without extension + expect(key).toBe('myfile'); + }); + it('Should generate key with custom key pattern and file pattern', () => { + // Define custom options + const options: KeygenOptions = { + filePattern: '{sensor}_{measurement}_{date}.jsonl', + keyPattern: '{sensor}/{measurement}/{date}', + }; + // Initialize Keygen with custom options + const keygen = new Keygen(options); + // Generate key for a sample file matching the pattern + const key = keygen.generateKey('/path/to/sensor1_temperature_2023-10-24.jsonl'); + // Expect the key to match the pattern with correct values + expect(key).toBe('sensor1/temperature/2023-10-24'); + }); + it('Should apply default values when placeholders are missing', () => { + // Define options with default values + const options: KeygenOptions = { + filePattern: '{sensor}_{measurement}_{date}.jsonl', + keyPattern: '{sensor}/{measurement}/{date}/{location}', + defaultValues: { location: 'defaultLocation' }, + }; + // Initialize Keygen with options + const keygen = new Keygen(options); + // Generate key for a sample file + const key = keygen.generateKey('/path/to/sensor1_temperature_2023-10-24.jsonl'); + // Expect the key to include the default location + expect(key).toBe('sensor1/temperature/2023-10-24/defaultLocation'); + }); + it('Should generate key with date placeholders', () => { + // Define options using date placeholders + const options: KeygenOptions = { + filePattern: '{sensor}_{measurement}.jsonl', + keyPattern: '{sensor}/{measurement}/{_year}/{_month}/{_day}', + }; + // Initialize Keygen + const keygen = new Keygen(options); + // Generate key for a sample file + const key = keygen.generateKey('/path/to/sensor1_temperature.jsonl'); + // Get current date components + const date = new Date(); + const year = date.getFullYear().toString(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + // Expect the key to include the correct date components + expect(key).toBe(`sensor1/temperature/${year}/${month}/${day}`); + }); + }); + describe('#Sad path', () => { + it('Should throw error when filename does not match pattern', () => { + // Define options with a specific file pattern + const options: KeygenOptions = { + filePattern: '{sensor}_{measurement}_{date}.jsonl', + }; + // Initialize Keygen + const keygen = new Keygen(options); + // Attempt to generate key with an invalid filename + expect(() => { + keygen.generateKey('/path/to/invalid_filename.jsonl'); + }).toThrow(/does not match the pattern/); + }); + it('Should include unknown placeholders in the key when not found in values', () => { + // Define options with a placeholder that is not in values + const options: KeygenOptions = { + filePattern: '{sensor}_{measurement}_{date}.jsonl', + keyPattern: '{sensor}/{measurement}/{date}/{unknown}', + }; + // Initialize Keygen + const keygen = new Keygen(options); + // Generate key + expect(() => { + keygen.generateKey('/path/to/sensor1_temperature_2023-10-24.jsonl'); + }).toThrow( + `Error generating a key based on pattern [{sensor}/{measurement}/{date}/{unknown}] for file [sensor1_temperature_2023-10-24.jsonl]: Placeholder [unknown] not found in values` + ); + }); + }); +}); diff --git a/packages/api/file-flinger/src/keygen/Keygen.ts b/packages/api/file-flinger/src/keygen/Keygen.ts new file mode 100644 index 00000000..549f8be3 --- /dev/null +++ b/packages/api/file-flinger/src/keygen/Keygen.ts @@ -0,0 +1,147 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { DebugLogger, SetContext, type LoggerInstance } from '@mdf.js/logger'; +import { merge } from 'lodash'; +import path from 'path'; +import { v4 } from 'uuid'; +import { DEFAULT_KEY_GEN_OPTIONS, InternalKeygenOptions, type KeygenOptions } from './types'; + +/** + * Key generator for the files + * The key generator is used to generate a key for a file based on a given pattern. + * The key pattern can contain placeholders that will be replaced by the actual values. + * The placeholders are enclosed in curly braces and the following are supported by default: + * - {_filename}: The name of the file + * - {_extension}: The extension of the file + * - {_timestamp}: The timestamp when the file was processed in milliseconds + * - {_date}: The date when the file was processed in the format YYYY-MM-DD + * - {_time}: The time when the file was processed in the format HH-mm-ss + * - {_datetime}: The date and time when the file was processed in the format YYYY-MM-DD_HH-mm-ss + * - {_year}: The year when the file was processed + * - {_month}: The month when the file was processed + * - {_day}: The day when the file was processed + * - {_hour}: The hour when the file was processed + * - {_minute}: The minute when the file was processed + * - {_second}: The second when the file was processed + * + * Other placeholders can be added by appling a custom file pattern over the file name, and using default values: + * - file name: mySensor_flowMeter1_2024-12-30_2024-12-31.jsonl + * - file pattern: {sensor}_{measurement}_{year}-{month}-{day}_{end} + * - default values: {source: 'myFileFlinger1'} + * - key pattern: {sensor}/{measurement}/{year}/{month}/{day}/data_{source} + * - key: mySensor/flowMeter1/2024/12/30/data_myFileFlinger1 + */ +export class Keygen { + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** The options for the key generator */ + private readonly options: InternalKeygenOptions; + /** + * Creates a new key generator instance with the given options. + * @param options The options for the key generator + * @param logger The logger instance for deep debugging tasks + */ + constructor(options?: KeygenOptions, logger?: LoggerInstance) { + this.options = merge({}, DEFAULT_KEY_GEN_OPTIONS, options); + // Stryker disable next-line all + this.logger = SetContext(logger || new DebugLogger(`mdf:fileFlinger:keygen`), 'keygen', v4()); + } + /** + * Parses a file name according to a given pattern and returns a map of placeholder values. + * @param fileName - The actual file name to parse. + * @returns A map where keys are placeholder names and values are the corresponding parts from the file name. + * @throws An error if the file name does not match the pattern. + */ + private parseFileName(fileName: string): Record { + // Return an empty object if no file pattern is defined + if (!this.options.filePattern) { + return {}; + } + // Escape special regex characters except for curly braces + const escapedPattern = this.options.filePattern.replace(/([.+^$[\]\\(){}|-])/g, '\\$1'); + + // Replace placeholders with named capture groups, excluding only the field separator '_' + const regexPattern = escapedPattern.replace(/\\{([^}]+)\\}/g, (_, key) => `(?<${key}>[^_]+)`); + + // Create a RegExp object with start and end anchors + const regex = new RegExp(`^${regexPattern}$`); + + // Match the file name against the regex pattern + const match = regex.exec(fileName); + + if (!match?.groups) { + const error = new Crash( + `Filename [${fileName}] does not match the pattern [${this.options.filePattern}]` + ); + this.logger.debug(error.message); + throw error; + } + + // Return the captured groups as a key-value map + return match.groups; + } + /** + * Generates a map of placeholder values based in a given file path and on the current date and + * time. + * @param filePath - The path to the file. + * @returns A map where keys are placeholder names and values are placeholders values. + */ + private generatePlaceholders(filePath: string): Record { + const date = new Date(); + const placeholders: Record = {}; + + placeholders['_filename'] = path.basename(filePath, path.extname(filePath)); + placeholders['_extension'] = path.extname(filePath); + placeholders['_timestamp'] = date.getTime().toString(); + placeholders['_date'] = date.toISOString().split('T')[0]; + placeholders['_time'] = date.toTimeString().split(' ')[0].replace(/:/g, '-'); + placeholders['_datetime'] = date + .toISOString() + .replace(/[-:]/g, '-') + .replace('T', '_') + .split('.')[0]; + placeholders['_year'] = date.getFullYear().toString(); + placeholders['_month'] = (date.getMonth() + 1).toString().padStart(2, '0'); + placeholders['_day'] = date.getDate().toString().padStart(2, '0'); + placeholders['_hour'] = date.getHours().toString().padStart(2, '0'); + placeholders['_minute'] = date.getMinutes().toString().padStart(2, '0'); + placeholders['_second'] = date.getSeconds().toString().padStart(2, '0'); + + // Merge parsed parts with default values + return placeholders; + } + /** + * Generates a key for a file based on a given pattern. + * @param filePath - The path to the file. + * @returns The generated key. + */ + public generateKey(filePath: string): string { + const fileName = path.basename(filePath); + + // Merge parsed parts with default values + const placeholders = { + ...this.options.defaultValues, + ...this.generatePlaceholders(filePath), + ...this.parseFileName(fileName), + }; + this.logger.debug(`Generated placeholders:\n ${JSON.stringify(placeholders, null, 2)}`); + const key = this.options.keyPattern.replace(/{([^}]+)}/g, (_, key) => { + if (!placeholders[key]) { + const error = new Crash( + `Error generating a key based on pattern [${this.options.keyPattern}] for file [${fileName}]: Placeholder [${key}] not found in values` + ); + this.logger.debug(error.message); + throw error; + } + return placeholders[key]; + }); + this.logger.debug(`Generated key: ${key}`); + return key; + } +} diff --git a/packages/api/file-flinger/src/keygen/index.ts b/packages/api/file-flinger/src/keygen/index.ts new file mode 100644 index 00000000..97b935ce --- /dev/null +++ b/packages/api/file-flinger/src/keygen/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Keygen'; +export * from './types'; diff --git a/packages/api/file-flinger/src/keygen/types/KeygenOptions.i.ts b/packages/api/file-flinger/src/keygen/types/KeygenOptions.i.ts new file mode 100644 index 00000000..b5d2816b --- /dev/null +++ b/packages/api/file-flinger/src/keygen/types/KeygenOptions.i.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export interface KeygenOptions { + /** File pattern to match the files to process */ + filePattern?: string | undefined; + /** + * Key pattern used to generate the key for the file in the pusher. + * The key pattern can contain placeholders that will be replaced by the actual values. + * The placeholders are enclosed in curly braces and the following are supported: + * - {_filename}: The name of the file + * - {_extension}: The extension of the file + * - {_timestamp}: The timestamp when the file was created or modified in milliseconds + * - {_date}: The date when the file was created or modified in the format YYYY-MM-DD + * - {_time}: The time when the file was created or modified in the format HH-mm-ss + * - {_datetime}: The date and time when the file was created or modified in the format + * YYYY-MM-DD_HH-mm-ss + * - {_year}: The year when the file was created or modified + * - {_month}: The month when the file was created or modified + * - {_day}: The day when the file was created or modified + * - {_hour}: The hour when the file was created or modified + * - {_minute}: The minute when the file was created or modified + * - {_second}: The second when the file was created or modified + */ + keyPattern?: string; + /** The default values for the keys */ + defaultValues?: Record; +} + +/** Internal key generator options */ +export type InternalKeygenOptions = Required> & { + filePattern: string | undefined; +}; diff --git a/packages/api/file-flinger/src/keygen/types/const.ts b/packages/api/file-flinger/src/keygen/types/const.ts new file mode 100644 index 00000000..900709ce --- /dev/null +++ b/packages/api/file-flinger/src/keygen/types/const.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { InternalKeygenOptions } from './KeygenOptions.i'; + +/** Default key generator options */ +export const DEFAULT_KEY_GEN_OPTIONS: InternalKeygenOptions = { + /** Match all files */ + filePattern: undefined, + /** Use the file name as the key */ + keyPattern: '{_filename}', + /** No default values */ + defaultValues: {}, +}; diff --git a/packages/api/file-flinger/src/keygen/types/index.ts b/packages/api/file-flinger/src/keygen/types/index.ts new file mode 100644 index 00000000..abb33aac --- /dev/null +++ b/packages/api/file-flinger/src/keygen/types/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './const'; +export * from './KeygenOptions.i'; diff --git a/packages/api/file-flinger/src/metrics/MetricsHandler.ts b/packages/api/file-flinger/src/metrics/MetricsHandler.ts new file mode 100644 index 00000000..839cce51 --- /dev/null +++ b/packages/api/file-flinger/src/metrics/MetricsHandler.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { Crash } from '@mdf.js/crash'; +import type { DoneListener, Limiter, MetaData } from '@mdf.js/tasks'; +import { Counter, Histogram, Registry } from 'prom-client'; + +/** Metric types */ +type MetricInstances = { + /** The total number of all jobs processed */ + jobsProcessed: Counter; + /** The total number errors processing jobs */ + jobsWithError: Counter; + /** File flinger jobs duration */ + jobsDuration: Histogram; +}; + +/** Metrics handler */ +export class MetricsHandler { + /** Metrics instances */ + private readonly metrics: MetricInstances; + /** The registry to register the metrics */ + public registry: Registry; + /** + * Create an instance of MetricsHandler + * @param limiter - The limiter instance to manage concurrency + */ + constructor(private readonly limiter: Limiter) { + this.registry = new Registry(); + this.registry.resetMetrics(); + this.metrics = this.defineMetrics(this.registry); + this.limiter.on('done', this.onDoneEventHandler); + } + /** Update the job processing metrics of a file flinger */ + private readonly onDoneEventHandler: DoneListener = ( + uuid: string, + result: any, + meta: MetaData, + error?: Crash + ): void => { + const type = meta.taskId; + this.metrics.jobsProcessed.inc({ type }); + if (error) { + this.metrics.jobsWithError.inc({ type }); + } + if (meta.duration) { + this.metrics.jobsDuration.observe({ type }, meta.duration); + } + }; + /** + * Define the metrics over a registry + * @param register - The registry to register the metrics + * @returns The metric instances + */ + private defineMetrics(register: Registry): MetricInstances { + return { + jobsProcessed: new Counter({ + name: 'api_all_job_processed_total', + help: 'The total number of all jobs processed', + labelNames: ['type'], + registers: [register], + }), + jobsWithError: new Counter({ + name: 'api_all_errors_job_processing_total', + help: 'The total number errors processing jobs', + labelNames: ['type'], + registers: [register], + }), + jobsDuration: new Histogram({ + name: 'api_publishing_job_duration_milliseconds', + help: 'Jobs duration', + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + labelNames: ['type'], + registers: [register], + }), + }; + } +} diff --git a/packages/api/file-flinger/src/metrics/index.ts b/packages/api/file-flinger/src/metrics/index.ts new file mode 100644 index 00000000..6f2a98cd --- /dev/null +++ b/packages/api/file-flinger/src/metrics/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './MetricsHandler'; diff --git a/packages/api/file-flinger/src/pusher/PusherWrapper.test.ts b/packages/api/file-flinger/src/pusher/PusherWrapper.test.ts new file mode 100644 index 00000000..b7ce85f6 --- /dev/null +++ b/packages/api/file-flinger/src/pusher/PusherWrapper.test.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { v4 } from 'uuid'; +import { PusherWrapper } from './PusherWrapper'; + +describe('#Sink #PlugWrapper', () => { + describe('#Happy path', () => { + it('Should wrap push/start/stop operations in the Pusher', async () => { + //@ts-expect-error - Test environment + const wrapper = new PusherWrapper({ + name: 'test', + componentId: 'test', + push: () => Promise.resolve(), + start: () => Promise.resolve(), + stop: () => Promise.resolve(), + close: () => Promise.resolve(), + }); + expect(wrapper).toBeDefined(); + expect(wrapper.name).toBe('test'); + expect(wrapper.componentId).toBe('test'); + expect(wrapper.push('', 'hi')).resolves.toEqual(undefined); + expect(wrapper.start()).resolves.toEqual(undefined); + expect(wrapper.stop()).resolves.toEqual(undefined); + expect(wrapper.close()).resolves.toEqual(undefined); + expect(wrapper.status).toBe('pass'); + }, 300); + }); + describe('#Sad path', () => { + it('Should throw an error if a pusher is not passed', () => { + try { + //@ts-expect-error - Test environment + new PusherWrapper(); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe('PusherWrapper requires a pusher instance'); + } + }, 300); + it('Should throw an error if the pusher does not implement the push method', () => { + try { + //@ts-expect-error - Test environment + new PusherWrapper({}); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe( + 'Pusher undefined does not implement the push method' + ); + } + }, 300); + it('Should throw an error if the pusher does not implement the push method properly', () => { + try { + //@ts-expect-error - Test environment + new PusherWrapper({ push: 3 }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe( + 'Pusher undefined does not implement the push method properly' + ); + } + }, 300); + it('Should throw an error if the plug does not implement the start method properly', () => { + try { + new PusherWrapper({ + name: 'myPusher', + push: () => Promise.resolve(), + //@ts-expect-error - Test environment + start: 3, + stop: () => Promise.resolve(), + close: () => Promise.resolve(), + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe( + 'Pusher myPusher not implement the start method properly' + ); + } + }, 300); + it('Should throw an error if the plug does not implement the stop method properly', () => { + try { + new PusherWrapper({ + name: 'myPusher', + push: () => Promise.resolve(), + //@ts-expect-error - Test environment + stop: 3, + start: () => Promise.resolve(), + close: () => Promise.resolve(), + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe( + 'Pusher myPusher not implement the stop method properly' + ); + } + }, 300); + it('Should throw an error if the plug does not implement the close method properly', () => { + try { + new PusherWrapper({ + name: 'myPusher', + push: () => Promise.resolve(), + stop: () => Promise.resolve(), + start: () => Promise.resolve(), + //@ts-expect-error - Test environment + close: 3, + }); + throw new Error(`Should throw an error`); + } catch (error) { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toBe( + 'Pusher myPusher not implement the close method properly' + ); + } + }, 300); + it(`Should reject the push operation if the pusher's push method fails`, done => { + //@ts-expect-error - Test environment + const wrapper = new PusherWrapper({ + name: 'test', + componentId: v4(), + push: () => Promise.reject(new Error('Test error')), + start: () => Promise.resolve(), + stop: () => Promise.resolve(), + close: () => Promise.resolve(), + }); + wrapper.on('error', error => { + expect(error).toBeInstanceOf(Crash); + expect((error as Crash).message).toEqual('Test error'); + done(); + }); + wrapper.push('dasd', 'hi').catch(() => {}); + }, 300); + }); +}); diff --git a/packages/api/file-flinger/src/pusher/PusherWrapper.ts b/packages/api/file-flinger/src/pusher/PusherWrapper.ts new file mode 100644 index 00000000..b44f85ca --- /dev/null +++ b/packages/api/file-flinger/src/pusher/PusherWrapper.ts @@ -0,0 +1,148 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, type Layer } from '@mdf.js/core'; +import { Crash, type Multi } from '@mdf.js/crash'; +import EventEmitter from 'events'; +import type { Pusher } from './types'; + +export class PusherWrapper extends EventEmitter implements Layer.App.Resource { + /** Indicate if the last operation was finished with error */ + private lastOperationError?: Crash | Multi; + /** Date of the last operation performed */ + private lastOperationDate?: Date; + /** Pusher push operation original */ + private readonly pushOriginal: (filePath: string, key: string) => Promise; + /** Pusher start operation original */ + private readonly startOriginal: () => Promise; + /** Pusher stop operation original */ + private readonly stopOriginal: () => Promise; + /** Pusher close operation original */ + private readonly closeOriginal: () => Promise; + /** + * Create a new instance of PusherWrapper + * @param pusher - pusher instance + */ + constructor(private readonly pusher: Pusher) { + super(); + if (!this.pusher) { + throw new Crash('PusherWrapper requires a pusher instance'); + } else if (!this.pusher.push) { + throw new Crash(`Pusher ${this.pusher.name} does not implement the push method`); + } else if (typeof this.pusher.push !== 'function') { + throw new Crash(`Pusher ${this.pusher.name} does not implement the push method properly`); + } else { + this.pushOriginal = this.pusher.push; + this.pusher.push = this.push; + } + if (typeof this.pusher.start !== 'function') { + throw new Crash(`Pusher ${this.pusher.name} not implement the start method properly`); + } else { + this.startOriginal = this.pusher.start; + this.pusher.start = this.start; + } + if (typeof this.pusher.stop !== 'function') { + throw new Crash(`Pusher ${this.pusher.name} not implement the stop method properly`); + } else { + this.stopOriginal = this.pusher.stop; + this.pusher.stop = this.stop; + } + if (typeof this.pusher.close !== 'function') { + throw new Crash(`Pusher ${this.pusher.name} not implement the close method properly`); + } else { + this.closeOriginal = this.pusher.close; + this.pusher.close = this.close; + } + } + /** Register an error in the pusher operation */ + private readonly onOperationError = (rawError: unknown): void => { + this.lastOperationError = Crash.from(rawError, this.pusher.componentId); + this.lastOperationDate = new Date(); + if (this.listenerCount('error') > 0) { + this.emit('error', this.lastOperationError); + } + }; + /** Register an error in the pusher operation */ + private readonly onOperationSuccess = (): void => { + this.lastOperationError = undefined; + this.lastOperationDate = new Date(); + }; + /** + * Perform the retry functionality for a promise + * @param task - promise to execute + * @param funcArgs - promise arguments + * @param options - control execution options + * @returns the promise result + */ + private async wrappedOperation( + task: (...args: any[]) => Promise, + funcArgs: any[] + ): Promise { + try { + const result = await task.bind(this.pusher)(...funcArgs); + this.onOperationSuccess(); + return result; + } catch (error) { + this.onOperationError(error); + throw error; + } + } + /** + * Perform the processing of the push operation + * @param filePath - The file path to push + * @param key - The key to use + * @returns the promise result + */ + public readonly push = async (filePath: string, key: string): Promise => { + await this.wrappedOperation(this.pushOriginal, [filePath, key]); + }; + /** Start the Pusher and the underlayer resources, making it available */ + public readonly start = async (): Promise => { + await this.wrappedOperation(this.startOriginal, []); + }; + /** Stop the Pusher and the underlayer resources */ + public readonly stop = async (): Promise => { + await this.wrappedOperation(this.stopOriginal, []); + }; + /** Close the Pusher and the underlayer resources, making it unavailable */ + public readonly close = async (): Promise => { + await this.wrappedOperation(this.closeOriginal, []); + }; + /** Component name */ + public get name(): string { + return this.pusher.name; + } + /** Component identification */ + public get componentId(): string { + return this.pusher.componentId; + } + /** + * Return the status of the stream in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + return { + ...this.pusher.checks, + [`${this.pusher.name}:lastOperation`]: [ + { + status: this.lastOperationError ? 'fail' : 'pass', + componentId: this.pusher.componentId, + componentType: 'plug', + observedValue: this.lastOperationError ? 'error' : 'ok', + observedUnit: 'result of last operation', + time: this.lastOperationDate ? this.lastOperationDate.toISOString() : undefined, + output: this.lastOperationError ? this.lastOperationError.trace() : undefined, + }, + ], + }; + } + /** Overall component status */ + public get status(): Health.Status { + return Health.overallStatus(this.checks); + } +} diff --git a/packages/api/file-flinger/src/pusher/index.ts b/packages/api/file-flinger/src/pusher/index.ts new file mode 100644 index 00000000..54c96d73 --- /dev/null +++ b/packages/api/file-flinger/src/pusher/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './PusherWrapper'; +export { Pusher } from './types'; diff --git a/packages/api/file-flinger/src/pusher/types/Pusher.i.ts b/packages/api/file-flinger/src/pusher/types/Pusher.i.ts new file mode 100644 index 00000000..346f5d62 --- /dev/null +++ b/packages/api/file-flinger/src/pusher/types/Pusher.i.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { Layer } from '@mdf.js/core'; + +/** Pusher interface */ +export interface Pusher extends Layer.App.Resource { + /** + * Push the file to the storage + * @param filePath - The file path to push + * @param key - The key to use + */ + push(filePath: string, key: string): Promise; +} diff --git a/packages/api/file-flinger/src/pusher/types/index.ts b/packages/api/file-flinger/src/pusher/types/index.ts new file mode 100644 index 00000000..eb3eb037 --- /dev/null +++ b/packages/api/file-flinger/src/pusher/types/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Pusher.i'; diff --git a/packages/api/file-flinger/src/types/FileFlingerOptions.i.ts b/packages/api/file-flinger/src/types/FileFlingerOptions.i.ts new file mode 100644 index 00000000..ab64ed15 --- /dev/null +++ b/packages/api/file-flinger/src/types/FileFlingerOptions.i.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { LoggerInstance } from '@mdf.js/logger'; +import type { EngineOptions } from '../engine/types'; +import type { KeygenOptions } from '../keygen'; +import type { WatcherOptions } from '../watcher'; + +export interface FileFlingerOptions + extends KeygenOptions, + Omit, + Omit { + /** Logger instance for deep debugging tasks */ + logger?: LoggerInstance; +} diff --git a/packages/api/file-flinger/src/types/const.ts b/packages/api/file-flinger/src/types/const.ts new file mode 100644 index 00000000..c10ca24b --- /dev/null +++ b/packages/api/file-flinger/src/types/const.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { LimiterOptions } from '@mdf.js/tasks'; +import { DEFAULT_ENGINE_OPTIONS } from '../engine'; +import { DEFAULT_KEY_GEN_OPTIONS } from '../keygen'; +import { DEFAULT_WATCHER_OPTIONS } from '../watcher'; +import type { FileFlingerOptions } from './FileFlingerOptions.i'; + +/** Default limiter options */ +export const DEFAULT_LIMITER_OPTIONS: LimiterOptions = { + concurrency: 1, + autoStart: true, + delay: 1000, + retryOptions: { + attempts: 3, + maxWaitTime: 60000, + timeout: 10000, + waitTime: 1000, + }, +}; + +/** Default file flinger options */ +export const DEFAULT_FILE_FLINGER_OPTIONS: FileFlingerOptions = { + ...DEFAULT_KEY_GEN_OPTIONS, + ...DEFAULT_WATCHER_OPTIONS, + ...DEFAULT_ENGINE_OPTIONS, + logger: undefined, +}; diff --git a/packages/api/file-flinger/src/types/index.ts b/packages/api/file-flinger/src/types/index.ts new file mode 100644 index 00000000..faa2c069 --- /dev/null +++ b/packages/api/file-flinger/src/types/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './const'; +export * from './FileFlingerOptions.i'; diff --git a/packages/api/file-flinger/src/watcher/Watcher.test.ts b/packages/api/file-flinger/src/watcher/Watcher.test.ts new file mode 100644 index 00000000..d3c7348f --- /dev/null +++ b/packages/api/file-flinger/src/watcher/Watcher.test.ts @@ -0,0 +1,307 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { EventEmitter } from 'events'; +import fs from 'fs'; +import { Watcher } from './Watcher'; + +// Mock chokidar +class WatchMock extends EventEmitter { + close = jest.fn(); + add = jest.fn(); + getWatched = jest.fn(); + unwatch = jest.fn(); +} + +jest.mock('chokidar', () => ({ + watch: jest.fn(() => new WatchMock()), +})); + +describe('#FileFlinger #Watcher', () => { + describe('#Happy path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should initialize with default options', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Verify that the Watcher initializes with default options + expect(watcher.name).toBe('watcher'); + expect(watcher.componentId).toBeDefined(); + expect(watcher.status).toBe('fail'); // Not ready yet + expect(watcher.errors).toHaveLength(0); + expect(watcher.checks).toEqual({ + 'watcher:errors': [ + { + status: 'pass', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: [], + observedUnit: 'observed errors', + time: undefined, + output: undefined, + }, + ], + 'watcher:status': [ + { + status: 'fail', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: 'fail', + observedUnit: 'status', + time: expect.any(String), + output: 'Watcher is not ready', + }, + ], + 'watcher:watcher': [ + { + status: 'warn', + componentId: expect.any(String), + componentType: 'watcher', + observedValue: undefined, + observedUnit: 'watched files', + time: expect.any(String), + output: 'Watcher is not started', + }, + ], + }); + }); + it('Should start the watcher without errors', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // If stop is called before start, it should not throw an error + await watcher.stop(); + // Start the watcher and expect no errors + await watcher.start(); + // if start is called again, it should not throw an error + await watcher.start(); + // Since 'ready' event hasn't been emitted yet, status should be 'fail' + expect(watcher.status).toBe('fail'); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should emit "add" event when a file is added', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Mock path of the added file + const mockPath = '/path/to/file.txt'; + // Spy on the 'add' event listener + const addListener = jest.fn(); + watcher.on('add', addListener); + // Simulate the 'add' event + (watcher['watcher'] as EventEmitter).emit('add', mockPath, {}); + // Expect the 'add' listener to be called with the mock path + expect(addListener).toHaveBeenCalledWith(mockPath); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should update status to "pass" when ready', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Simulate the 'ready' event + (watcher['watcher'] as EventEmitter).emit('ready'); + // Expect the status to be 'pass' after ready + expect(watcher.status).toBe('pass'); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should stop the watcher without errors', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Stop the watcher + await watcher.stop(); + // Expect the watcher to be not ready after stopping + expect(watcher.status).toBe('fail'); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should close the watcher without errors', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Close the watcher + await watcher.close(); + // Expect the watcher instance to be undefined after closing + expect(watcher['watcher']).toBeUndefined(); + }); + it('Should return correct health checks', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Simulate the 'ready' event + (watcher['watcher'] as EventEmitter).emit('ready'); + // Get the health checks + const checks = watcher.checks; + // Expect health checks to have 'pass' status + expect(checks[`${watcher.name}:status`][0].status).toBe(Health.STATUS.PASS); + // Clean up by closing the watcher + await watcher.close(); + }); + it(`Should manage unlink and change events`, async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // @ts-expect-error - Spy on the logger debug method + const debug = jest.spyOn(watcher.logger, 'debug'); + // Mock path of the unlinked file + const mockPath = '/path/to/file.txt'; + // Simulate the 'unlink' event + (watcher['watcher'] as EventEmitter).emit('unlink', mockPath, {}); + expect(debug).toHaveBeenCalledTimes(1); + // Simulate the 'change' event + (watcher['watcher'] as EventEmitter).emit('change', mockPath, {}); + expect(debug).toHaveBeenCalledTimes(2); + // Clean up by closing the watcher + await watcher.close(); + }); + it(`Should emit an error event when "add", "unlink" or "change" receive an error instead of a path`, async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Spy on the 'error' event listener + const error = jest.fn(); + watcher.on('error', error); + // Simulate the 'add' event with an error + (watcher['watcher'] as EventEmitter).emit('add', new Error('Test error'), {}); + expect(error).toHaveBeenCalledTimes(1); + // Simulate the 'unlink' event with an error + (watcher['watcher'] as EventEmitter).emit('unlink', new Error('Test error'), {}); + expect(error).toHaveBeenCalledTimes(2); + // Simulate the 'change' event with an error + (watcher['watcher'] as EventEmitter).emit('change', new Error('Test error'), {}); + expect(error).toHaveBeenCalledTimes(3); + // Clean up by closing the watcher + await watcher.close(); + }); + }); + describe('Sad path', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Should handle errors emitted by the watcher', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + // Mock an error + const mockError = new Error('Test error'); + // Spy on the 'error' event listener + const errorListener = jest.fn(); + watcher.on('error', errorListener); + // Simulate the 'error' event + (watcher['watcher'] as EventEmitter).emit('error', mockError); + // Expect the error listener to be called with a Crash instance + expect(errorListener).toHaveBeenCalled(); + const errorArg = errorListener.mock.calls[0][0]; + expect(errorArg).toBeInstanceOf(Crash); + expect(errorArg.message).toContain('Watcher error'); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should limit the number of stored errors', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance with a small maxErrors limit + const maxErrors = 5; + const watcher = new Watcher({ maxErrors }); + watcher.on('error', () => {}); + // Start the watcher + await watcher.start(); + // Mock an error + const mockError = new Error('Test error'); + // Simulate multiple 'error' events exceeding maxErrors + for (let i = 0; i < maxErrors + 3; i++) { + (watcher['watcher'] as EventEmitter).emit('error', mockError); + } + // Expect the error stack to not exceed maxErrors + expect(watcher.errors.length).toBe(maxErrors); + // Clean up by closing the watcher + await watcher.close(); + }); + it('Should not throw error when closing an already closed watcher', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + + // Start and close the watcher + await watcher.start(); + await watcher.close(); + + // Attempt to close the watcher again + await expect(watcher.close()).resolves.toBeUndefined(); + }); + it('Should handle errors when closing the watcher', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => true } as fs.Stats); + // Create a new Watcher instance + const watcher = new Watcher(); + // Start the watcher + await watcher.start(); + //@ts-expect-error - Mocking private method + jest.spyOn(watcher.watcher, 'close').mockRejectedValue(new Error('Close error')); + // Expect the close method to throw a Crash error + await expect(watcher.close()).rejects.toThrow('Error closing the watcher: Close error'); + }); + it('Should throw a error in instance creation if watch folder is not a string or string array, not exists or is not a folder', () => { + expect(() => { + new Watcher({ watchPath: '/path/to/folder' }); + }).toThrow('Watch path does not exist: /path/to/folder'); + expect(() => { + // @ts-expect-error - Invalid watch path + new Watcher({ watchPath: null }); + }).toThrow('Watcher must have a watch path'); + expect(() => { + // @ts-expect-error - Invalid watch path + new Watcher({ watchPath: 123 }); + }).toThrow('Invalid watch path: 123'); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'statSync').mockReturnValue({ isDirectory: () => false } as fs.Stats); + expect(() => { + new Watcher({ watchPath: '/path/to/file.txt' }); + }).toThrow('Watch path is not a directory: /path/to/file.txt'); + }); + }); +}); diff --git a/packages/api/file-flinger/src/watcher/Watcher.ts b/packages/api/file-flinger/src/watcher/Watcher.ts new file mode 100644 index 00000000..edf20221 --- /dev/null +++ b/packages/api/file-flinger/src/watcher/Watcher.ts @@ -0,0 +1,286 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, type Layer } from '@mdf.js/core'; +import { Crash, type Multi } from '@mdf.js/crash'; +import { DebugLogger, SetContext, type LoggerInstance } from '@mdf.js/logger'; +import { deCycle } from '@mdf.js/utils'; +import { watch, type ChokidarOptions, type FSWatcher } from 'chokidar'; +import EventEmitter from 'events'; +import { existsSync, Stats, statSync } from 'fs'; +import { merge } from 'lodash'; +import path from 'path'; +import { + DEFAULT_FS_WATCHER_OPTIONS, + DEFAULT_WATCHER_OPTIONS, + type InternalWatcherOptions, + type WatcherOptions, +} from './types'; + +export declare interface Watcher { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Multi | Error) => void): this; + /** + * Add a listener for the status event, emitted when the component status changes. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the add event, emitted when a file is added. + * @param event - `add` event + * @param listener - Add event listener + * @event + */ + on(event: 'add', listener: (path: string) => void): this; +} + +export class Watcher extends EventEmitter implements Layer.App.Resource { + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** The options for the watcher */ + private readonly options: InternalWatcherOptions; + /** The watcher instance */ + private watcher?: FSWatcher; + /** The options for the chokidar watcher */ + private readonly fsWatcherOptions: ChokidarOptions; + /** Flag to indicate if the watcher is ready */ + private ready: boolean = false; + /** The error stack for the watcher */ + private readonly errorStacks: string[] = []; + /** + * Create a new watcher instance with the given options + * @param options - The watcher options + * @param logger - The logger instance + */ + constructor(options?: WatcherOptions, logger?: LoggerInstance) { + super(); + this.options = merge({}, DEFAULT_WATCHER_OPTIONS, options); + // Stryker disable next-line all + this.logger = SetContext( + logger || new DebugLogger(`mdf:fileFlinger:watcher`), + 'keygen', + this.options.componentId + ); + this.fsWatcherOptions = { + ...DEFAULT_FS_WATCHER_OPTIONS, + cwd: this.options.cwd, + }; + this.logger.silly( + `Watcher created with options: ${JSON.stringify(deCycle(this.options), null, 2)}` + ); + if (!this.options.watchPath) { + throw new Crash(`Watcher must have a watch path`, this.options.componentId); + } + const watchedPaths = Array.isArray(this.options.watchPath) + ? this.options.watchPath + : [this.options.watchPath]; + for (const folderPath of watchedPaths) { + // Path must be a string + if (typeof folderPath !== 'string') { + throw new Crash(`Invalid watch path: ${folderPath}`, this.options.componentId); + } + const completePath = path.resolve(this.options.cwd ?? '', folderPath); + // Path must exist + if (!existsSync(completePath)) { + throw new Crash(`Watch path does not exist: ${completePath}`, this.options.componentId); + } + // Path must be a directory + const stats = statSync(completePath); + if (!stats.isDirectory()) { + throw new Crash(`Watch path is not a directory: ${completePath}`, this.options.componentId); + } + } + } + /** + * Perform the subscription to the events from the watcher + * @param watcher - The watcher instance + */ + private wrappingEvents(watcher: FSWatcher): FSWatcher { + watcher.on('error', this.onErrorEventHandler); + watcher.on('ready', this.onReadyEventHandler); + watcher.on('add', this.onAddEventHandler); + watcher.on('change', this.onChangEventHandler); + watcher.on('unlink', this.onUnlinkEventHandler); + return watcher; + } + /** + * Perform the unsubscription to the events from the watcher + * @param watcher - The watcher instance + */ + private unwrappingEvents(watcher: FSWatcher): FSWatcher { + watcher.off('error', this.onErrorEventHandler); + watcher.off('ready', this.onReadyEventHandler); + watcher.off('add', this.onAddEventHandler); + watcher.off('change', this.onChangEventHandler); + watcher.off('unlink', this.onUnlinkEventHandler); + return watcher; + } + /** + * Event handler for the error event + * @param rawError - The error that occurred + */ + private readonly onErrorEventHandler = (rawError: unknown) => { + const cause = Crash.from(rawError); + const error = new Crash(`Watcher error: ${cause.message}`, { cause }); + this.errorStacks.push(`${error.date.toISOString()} - ${error.message}`); + if (this.errorStacks.length > this.options.maxErrors) { + this.errorStacks.shift(); + } + this.emit('error', error); + }; + /** Event handler for the ready event */ + private readonly onReadyEventHandler = () => { + this.ready = true; + this.logger.debug('Watcher ready'); + }; + /** + * Event handler for the add event + * @param path - The path of the file added + */ + private readonly onAddEventHandler = (path: string | Error, stats?: Stats) => { + if (path instanceof Error) { + this.onErrorEventHandler(path); + return; + } + if (!stats) { + this.logger.silly(`Stats: ${JSON.stringify(stats, null, 2)}`); + } + this.logger.debug(`File added: ${path}`); + this.emit('add', path); + }; + /** + * Event handler for the change event + * @param path - The path of the file changed + */ + private readonly onChangEventHandler = (path: string | Error, stats?: Stats) => { + if (path instanceof Error) { + this.onErrorEventHandler(path); + return; + } + if (!stats) { + this.logger.silly(`Stats: ${JSON.stringify(stats, null, 2)}`); + } + this.logger.debug(`File changed: ${path}`); + }; + /** + * Event handler for the unlink event + * @param path - The path of the file unlinked + */ + private readonly onUnlinkEventHandler = (path: string | Error, stats?: Stats) => { + if (path instanceof Error) { + this.onErrorEventHandler(path); + return; + } + if (!stats) { + this.logger.silly(`Stats: ${JSON.stringify(stats, null, 2)}`); + } + this.logger.debug(`File unlinked: ${path}`); + }; + /** Get the status of the watcher */ + public get status(): Health.Status { + return this.ready && !this.errorStacks.length ? 'pass' : 'fail'; + } + /** Get the name of the watcher */ + public get name(): string { + return this.options.name; + } + /** Get the component identifier */ + public get componentId(): string { + return this.options.componentId; + } + /** Get the error stack */ + public get errors(): string[] { + return this.errorStacks; + } + /** Get the health checks */ + public get checks(): Health.Checks { + const errorsLength = this.errorStacks.length; + const lastError = errorsLength ? this.errorStacks[errorsLength - 1] : undefined; + const lastErrorDate = lastError ? lastError.split(' - ')[0] : undefined; + return { + [`${this.options.name}:errors`]: [ + { + status: errorsLength ? Health.STATUS.FAIL : Health.STATUS.PASS, + componentId: this.options.componentId, + componentType: 'watcher', + observedValue: this.errorStacks, + observedUnit: 'observed errors', + time: lastErrorDate, + output: errorsLength ? `Some error occurred in the watcher` : undefined, + }, + ], + [`${this.options.name}:status`]: [ + { + status: this.status, + componentId: this.options.componentId, + componentType: 'watcher', + observedValue: this.status, + observedUnit: 'status', + time: new Date().toISOString(), + output: this.status === 'fail' ? 'Watcher is not ready' : undefined, + }, + ], + [`${this.options.name}:watcher`]: [ + { + status: this.watcher ? Health.STATUS.PASS : Health.STATUS.WARN, + componentId: this.options.componentId, + componentType: 'watcher', + observedValue: this.watcher ? this.watcher.getWatched() : undefined, + observedUnit: 'watched files', + time: new Date().toISOString(), + output: this.watcher ? undefined : 'Watcher is not started', + }, + ], + }; + } + /** Start the watcher */ + public async start(): Promise { + if (this.watcher) { + this.logger.debug('Watcher already started'); + const watched = this.watcher.getWatched(); + if (!watched || !Object.keys(watched).length) { + this.logger.debug('No files watched'); + this.watcher.add(this.options.watchPath); + } + return; + } + this.watcher = this.wrappingEvents(watch([], this.fsWatcherOptions)); + this.watcher.add(this.options.watchPath); + } + /** Stop the watcher */ + public async stop(): Promise { + if (!this.watcher) { + this.logger.debug('Watcher already stopped'); + return; + } + this.watcher.unwatch(this.options.watchPath); + this.ready = false; + } + /** Close the watcher */ + public async close(): Promise { + if (!this.watcher) { + this.logger.debug('Watcher already stopped'); + return; + } + this.unwrappingEvents(this.watcher); + try { + await this.watcher.close(); + this.watcher = undefined; + this.ready = false; + } catch (rawError) { + const cause = Crash.from(rawError); + throw new Crash(`Error closing the watcher: ${cause.message}`, { cause }); + } + } +} diff --git a/packages/api/file-flinger/src/watcher/index.ts b/packages/api/file-flinger/src/watcher/index.ts new file mode 100644 index 00000000..6f6baf90 --- /dev/null +++ b/packages/api/file-flinger/src/watcher/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './types'; +export * from './Watcher'; diff --git a/packages/api/file-flinger/src/watcher/types/WatcherOptions.i.ts b/packages/api/file-flinger/src/watcher/types/WatcherOptions.i.ts new file mode 100644 index 00000000..ab14741c --- /dev/null +++ b/packages/api/file-flinger/src/watcher/types/WatcherOptions.i.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Watcher options */ +export interface WatcherOptions { + /** The name of the watcher */ + name?: string; + /** The component identifier */ + componentId?: string; + /** The path to watch */ + watchPath?: string | string[]; + /** The base path to use */ + cwd?: string | undefined; + /** Max number of errors to store */ + maxErrors?: number; +} + +/** Internal watcher options */ +export type InternalWatcherOptions = Required> & { + cwd: string | undefined; +}; diff --git a/packages/api/file-flinger/src/watcher/types/const.ts b/packages/api/file-flinger/src/watcher/types/const.ts new file mode 100644 index 00000000..e3b1ca52 --- /dev/null +++ b/packages/api/file-flinger/src/watcher/types/const.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import type { ChokidarOptions } from 'chokidar'; +import { v4 } from 'uuid'; +import type { InternalWatcherOptions } from './WatcherOptions.i'; + +/** Default watcher options */ +export const DEFAULT_WATCHER_OPTIONS: InternalWatcherOptions = { + /** Watcher name */ + name: 'watcher', + /** Component identifier */ + componentId: v4(), + /** Watch folder */ + watchPath: './data/archive', + /** Base path */ + cwd: undefined, + /** Max number of errors to store */ + maxErrors: 10, +}; + +/** Default fs watcher options */ +export const DEFAULT_FS_WATCHER_OPTIONS: ChokidarOptions = { + persistent: true, + ignored: undefined, + ignoreInitial: false, + followSymlinks: true, + cwd: undefined, + usePolling: false, + alwaysStat: false, + depth: undefined, + awaitWriteFinish: { + stabilityThreshold: 10000, + pollInterval: 1000, + }, + ignorePermissionErrors: false, + atomic: true, +}; diff --git a/packages/api/file-flinger/src/watcher/types/index.ts b/packages/api/file-flinger/src/watcher/types/index.ts new file mode 100644 index 00000000..1cfb6865 --- /dev/null +++ b/packages/api/file-flinger/src/watcher/types/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './const'; +export * from './WatcherOptions.i'; diff --git a/packages/api/file-flinger/stryker.conf.js b/packages/api/file-flinger/stryker.conf.js new file mode 100644 index 00000000..deb85cd0 --- /dev/null +++ b/packages/api/file-flinger/stryker.conf.js @@ -0,0 +1,8 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + module.exports = require('@mdf.js/repo-config').getStrykerConfig(__dirname); + diff --git a/packages/api/file-flinger/tsconfig.build.json b/packages/api/file-flinger/tsconfig.build.json new file mode 100644 index 00000000..abaefaf4 --- /dev/null +++ b/packages/api/file-flinger/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "types": ["node"] + }, + "exclude": ["src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/packages/api/file-flinger/tsconfig.json b/packages/api/file-flinger/tsconfig.json new file mode 100644 index 00000000..a812543b --- /dev/null +++ b/packages/api/file-flinger/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mdf.js/repo-config/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + }, + "exclude": ["node_modules", "dist"] +} diff --git a/packages/api/file-flinger/tsconfig.lint.json b/packages/api/file-flinger/tsconfig.lint.json new file mode 100644 index 00000000..56fd5680 --- /dev/null +++ b/packages/api/file-flinger/tsconfig.lint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/api/file-flinger/tsconfig.spec.json b/packages/api/file-flinger/tsconfig.spec.json new file mode 100644 index 00000000..9d286887 --- /dev/null +++ b/packages/api/file-flinger/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["src/**/*.ts", "jest.config.js"] +} diff --git a/packages/api/file-flinger/typedoc.json b/packages/api/file-flinger/typedoc.json new file mode 100644 index 00000000..6a621e5d --- /dev/null +++ b/packages/api/file-flinger/typedoc.json @@ -0,0 +1,34 @@ +{ + "extends": ["../../../typedoc.json"], + "entryPoints": [ + "src/index.ts" + ], + "tsconfig": "./tsconfig.build.json", + "entryPointStrategy": "resolve", + "exclude": [ + "**/dist/**", + "**/node_modules", + "src/**/*.test.ts", + "src/**/test/*.*" + ], + "externalPattern": ["**/node_modules/**"], + "excludeExternals": true, + "excludePrivate": true, + "excludeProtected": true, + "excludeInternal": true, + + "out": "../../../docs/api/file-flinger", + "markdownItOptions": { + "html": true, + "linkify": true, + }, + "name": "@mdf.js/file-flinger", + "includeVersion": false, + "disableSources": true, + "excludeTags": [], + "readme": "README.md", + "categorizeByGroup": false, + "categoryOrder": [], + "cleanOutputDir": true, + "gitRevision": "master", +} diff --git a/packages/api/firehose/README.md b/packages/api/firehose/README.md index cb225493..1c04cefa 100644 --- a/packages/api/firehose/README.md +++ b/packages/api/firehose/README.md @@ -1,8 +1,9 @@ -# **@mdf.js** +# **@mdf.js/firehose** [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -12,8 +13,8 @@

                          -

                          Mytra Development Framework - @mdf.js

                          -
                          Typescript tools for development
                          +

                          Mytra Development Framework - @mdf.js/firehose

                          +
                          Module designed to facilitate the creation of customized data streaming pipelines.
                          @@ -21,22 +22,592 @@ ## **Table of contents** -- [**@mdf.js**](#mdfjs) +- [**@mdf.js/firehose**](#mdfjsfirehose) - [**Table of contents**](#table-of-contents) - [**Introduction**](#introduction) - [**Installation**](#installation) - - [**Information**](#information) - [**Use**](#use) + - [**Typing your own firehose**](#typing-your-own-firehose) + - [**Jobs**](#jobs) + - [**Creating a Plug**](#creating-a-plug) + - [**Plugs.Source.Flow**](#plugssourceflow) + - [**Plugs.Source.Sequence**](#plugssourcesequence) + - [**Plugs.Source.CreditFlow**](#plugssourcecreditflow) + - [**Plugs.Sink.Tap**](#plugssinktap) + - [**Plugs.Sink.Jet**](#plugssinkjet) + - [**Creating a Strategy**](#creating-a-strategy) + - [**Event Handling**](#event-handling) + - [**Firehose Configuration and Lifecycle**](#firehose-configuration-and-lifecycle) + - [**Metrics and Health Checks**](#metrics-and-health-checks) - [**License**](#license) ## **Introduction** +**@mdf.js/firehose** is a robust module within the @mdf.js ecosystem, designed to create customized data streaming pipelines. It provides a versatile framework for constructing complex data processing workflows, enabling developers to define custom plugs and strategies for handling diverse data streams. + +Before delving into the documentation, it’s essential to understand the core concepts within @mdf.js/firehose: + +- **Plugs**: Plugs act as the endpoints of data pipelines, responsible for receiving and sending data to or from the pipeline. They adapt the data stream to the requirements of source or destination systems, aligning with each system’s flow needs. Plugs can be categorized as inputs (**Source**) or outputs (**Sink**) and vary by flow conditions supported by the connecting systems: + + - **Source**: + - **Flow**: Plugs that allow continuous data entry; this flow can be paused or restarted based on the pipeline’s state. Typical for data streaming systems like message brokers. + - **Sequence**: Plugs that enable data entry in a sequential flow, where the pipeline requests data in specified quantities from the plug. Common for data storage systems such as databases. + - **CreditFlow**: Continuous flow plugs that require a credit system to receive data. Common in data streaming systems that necessitate authorization to continue receiving data. + - **Sink**: + - **Tap**: Plugs that process data one unit at a time, meaning the pipeline calls the plug’s write method for a single data instance. Common in systems that do not support bulk operations. + - **Jet**: Plugs that handle data in batches, where the pipeline calls the plug’s write method with multiple data instances. Typical for systems enabling bulk processing. + +- **Jobs**: Jobs are instances that transport data and metadata through the pipeline. They manage the data flow between plugs, ensuring correct processing and pipeline state maintenance. Source plugs are notified when a job completes, allowing them to “acknowledge” data from the source system. Jobs can carry additional metadata or processing information, which plugs and strategies can utilize to make decisions or perform specific actions. + +- **Strategies**: Strategies provide customizable, `type`-based functions that define how to transform job-carried data. Strategies can filter, transform, enrich, or aggregate data as required. They can be chained to build complex data processing workflows, allowing developers to create tailored data pipelines. + +This entire ecosystem leverages Node.js streams to build high-performance data processing pipelines capable of efficiently handling large volumes of data. + +![Firehose](./media/firehose-diagram.svg) + +Other key features of **@mdf.js/firehose** include: + +- **Error Handling**: The module provides a robust error handling mechanism, allowing developers to define custom error handling strategies for different scenarios. +- **Logging**: The module supports logging, enabling developers to track the pipeline’s execution and performance. +- **Metrics**: The module provides metrics for monitoring pipeline performance, allowing developers to track data processing efficiency and identify bottlenecks. These metrics are offered in the Prometheus format, using the `prom-client` library. +- **Multi-Threaded Processing**: The module supports multi-threaded processing, allowing developers to leverage the full potential of multi-core processors thanks to the **@mdf.js/service-registry** module. +- **Tooling**: The module provides a set of tools to facilitate the creation of custom plugs and strategies, including a plug generator and a strategy generator. These plugs could be based on the **@mdf.js** providers. + ## **Installation** -## **Information** +- **npm**: + +```bash +npm install @mdf.js/firehose +``` + +- **yarn**: + +```bash +yarn add @mdf.js/firehose +``` ## **Use** +### **Typing your own firehose** + +The `Firehose` class, along with the `Jobs`, `Plugs`, and `Strategies` classes, are generic classes that allow you to define the types of the data that will be processed by the pipeline. This is useful to ensure that the data is correctly typed throughout the pipeline. + +There are four types that you can define in the `Firehose` class: + +- `Type` (`Type extends string = string`): Job type, used as a selector for strategies in job processing. +- `Data` (`Data = any`): Data type that will be processed by the pipeline. +- `CustomHeaders` (`CustomHeaders extends Record = {}`): Custom headers, used to pass specific information for job processors. +- `CustomOptions` (`CustomOptions extends Record = {}`): Custom options, used to pass specific information for job processors. + +For example, if you want to create a pipeline that processes data of type `MyData`, you can define the `Firehose` class as follows: + +```typescript +import { Firehose } from '@mdf.js/firehose'; + +type MyJobType = 'TypeOne' | 'TypeTwo'; +type MyJobDataType = T extends 'TypeOne' + ? { fieldOne: string } + : T extends 'TypeTwo' + ? { fieldTwo: number } + : never; +type JobHeaders = { headerOne: string }; +type JobOptions = { optionOne: string }; + +const myFirehose = new Firehose, JobHeaders, JobOptions>('MyFirehose', { + sources: [/* Your source plugs */], + sinks: [/* Your sink plugs */], + strategies: { + TypeOne: [/* Your strategies for TypeOne */], + TypeTwo: [/* Your strategies for TypeTwo */], + }, +}); +``` + +### **Jobs** + +As previously mentioned, jobs are instances that transport data and metadata through the pipeline. They manage the data flow between plugs, ensuring correct processing and pipeline state maintenance. `Source` and `Sink` plugs do not use `Jobs` directly to avoid the need to create and manage them manually. Instead, they use the `JobRequest`, in the case of `Source` plugs, and the `JobObject`, in the case of `Sink` plugs. + +Both of them can be typed using the `Firehose` class that we previously defined. The `JobRequest` and `JobObject` types are defined as follows: + +- `JobRequest` is an object that contains the data and metadata that the `Source` plug needs to create a job. + +```typescript +export interface JobRequest< + Type extends string = string, + Data = unknown, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> { + /** Job type identification, used to identify specific job handlers to be applied */ + type?: Type; + /** User job request identifier, defined by the user */ + jobUserId: string; + /** Job payload */ + data: Data; + /** Job meta information, used to pass specific information for job processors */ + options?: Jobs.Options; +} +``` + +An example of a `JobRequest` could be: + +```typescript +const jobRequest: JobRequest, JobHeaders, JobOptions> = { + type: 'TypeOne', + jobUserId: '1234', + data: { fieldOne: 'value' }, + options: { headers: { headerOne: 'value' }, optionOne: 'value' }, +}; +``` + +- `JobObject` is an object that contains the data and metadata that the `Sink` plug needs to process a job. + +```typescript +export interface JobObject< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> extends JobRequest { + /** Job type identification, used to identify specific job handlers to be applied */ + type: Type; + /** Unique job processing identification */ + uuid: string; + /** + * Unique user job request identification, generated by UUID V5 standard and based on jobUserId + */ + jobUserUUID: string; + /** Job status */ + status: Jobs.Status; +} +``` + +An example of a `JobObject` could be: + +```typescript +const jobObject: JobObject, JobHeaders, JobOptions> = { + type: 'TypeOne', + uuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + jobUserId: '1234', + jobUserUUID: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + data: { fieldOne: 'value' }, + options: { headers: { headerOne: 'value' }, optionOne: 'value' }, + status: 'pending', +}; +``` + +> When the `Sink` plug completes processing a job (using the `single` method for `Tap` plugs or the `single`/`multi` methods for `Jet` plugs), the `Firehose` will notify the `Source` plug that the job has been processed by calling the method `postConsume` with the `jobUserId` as a parameter. + +### **Creating a Plug** + +The first step in building a data streaming pipeline is creating a plug. Plugs act as the endpoints of the pipeline, handling data input and output. To create a plug, you need to create your own class that implements the `Source` or `Sink` interface. All the `Source` and `Sink` interfaces extend the interface `Layer.App.Resource`, including some additional methods and events to handle the data flow. + +- **Source** + - **Events**: + - `data`: Event that is emitted when the plug has data to be processed. The emitted data is a `JobRequest` object. + - **Methods**: + - `postConsume(jobId: string): Promise`: Method that is called when the plug has processed a job. The parameter is the `jobUserId` of the processed job. This method must return a promise that resolves to the same `jobUserId` of the processed job or `undefined`. If `undefined` is resolved, the `Firehose` will understand that the job has not been found in the source plug due to an error. This is an important method to implement to ensure that the pipeline is working correctly by cleaning from the source (if needed) when the job has been processed. + - **Types of Source Plugs**: + - **Flow**: + - `init(): void`: Method that is called when the plug is initialized and indicates that the plug can start emitting data. + - `pause(): void`: Method that is called when the plug is paused and indicates that the plug should stop emitting data. + - **Sequence**: + - `ingestData(size: number): Promise`: Method that is called when the plug needs to ingest data. The parameter is the quantity of data that the plug needs to ingest. This method must return a promise that resolves to a single `JobRequest` object or an array of `JobRequest` objects. It's not necessary to resolve the exact quantity of data requested, but the plug must resolve at least one `JobRequest` object. If the plug doesn't have more data to ingest, it must block the promise until it has more data to ingest. + - **CreditFlow**: + - `addCredits(credits: number): Promise`: Method that is called to add credits to the plug. The parameter is the quantity of credits to add. This method must return a promise that resolves when the credits have been added to the plug, returning the number of credits that the plug has. Each emitted data will consume one credit. If the plug doesn't have credits, it must wait until `addCredits` is called to emit more data. + +- **Sink** + - **Methods**: + - **Tap**: + - `single(job: JobObject): Promise`: Method that is called to process a single job. + - **Jet**: + - `single(job: JobObject): Promise`: Method that is called to process a single job. + - `multi(jobs: JobObject[]): Promise`: Method that is called to process multiple jobs. + +As a `Layer.App.Resource`, the `Plugs` should include health information in the `checks` and `status` properties. The `status` property should be `pass` if the plug is healthy and `fail` if the plug is not healthy. The `checks` property should include an array of checks that the plug must pass to be considered healthy. If you use the `@mdf.js` framework to create your plug, you can combine the provider health information with the plug health information. + +#### **Plugs.Source.Flow** + +```typescript +import { Plugs, JobRequest } from '@mdf.js/firehose'; +import { EventEmitter } from 'events'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +/** Class that implements the Plugs.Source.Flow interface */ +class MySourcePlug extends EventEmitter implements Plugs.Source.Flow { + /** Constructor */ + constructor() { + super(); + } + /** Method that is called when the firehose has processed a job */ + public postConsume(jobId: string): Promise { + // Implementation + } + /** Method that is called when the plug is initialized and indicates that the plug can start emitting data */ + public init(): void { + // Implementation + } + /** Method that is called when the plug is paused and indicates that the plug should stop emitting data */ + public pause(): void { + // Implementation + } + /** Start the plug and the underlying provider */ + public async start(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider */ + public async stop(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider and clean the resources */ + public async close(): Promise { + // Implementation + } + /** Prometheus registry to store the metrics of the plug */ + public get metrics(): Registry { + // Implementation + } + /** Plug health status */ + public get status(): Health.Status { + // Implementation + } + /** Plug health checks */ + public get checks(): Health.Checks { + // Implementation + } +} +``` + +#### **Plugs.Source.Sequence** + +```typescript +import { Plugs, JobRequest } from '@mdf.js/firehose'; +import { EventEmitter } from 'events'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +/** Class that implements the Plugs.Source.Sequence interface */ +class MySourcePlug extends EventEmitter implements Plugs.Source.Sequence { + /** Constructor */ + constructor() { + super(); + } + /** Method that is called when the firehose has processed a job */ + public postConsume(jobId: string): Promise { + // Implementation + } + /** Method that is called when the plug needs to ingest data */ + public ingestData(size: number): Promise { + // Implementation + } + /** Start the plug and the underlying provider */ + public async start(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider */ + public async stop(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider and clean the resources */ + public async close(): Promise { + // Implementation + } + /** Prometheus registry to store the metrics of the plug */ + public get metrics(): Registry { + // Implementation + } + /** Plug health status */ + public get status(): Health.Status { + // Implementation + } + /** Plug health checks */ + public get checks(): Health.Checks { + // Implementation + } +} +``` + +#### **Plugs.Source.CreditFlow** + +```typescript +import { Plugs, JobRequest } from '@mdf.js/firehose'; +import { EventEmitter } from 'events'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +/** Class that implements the Plugs.Source.CreditFlow interface */ +class MySourcePlug extends EventEmitter implements Plugs.Source.CreditFlow { + /** Constructor */ + constructor() { + super(); + } + /** Method that is called when the firehose has processed a job */ + public postConsume(jobId: string): Promise { + // Implementation + } + /** Method that is called to add credits to the plug */ + public addCredits(credits: number): Promise { + // Implementation + } + /** Start the plug and the underlying provider */ + public async start(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider */ + public async stop(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider and clean the resources */ + public async close(): Promise { + // Implementation + } + /** Prometheus registry to store the metrics of the plug */ + public get metrics(): Registry { + // Implementation + } + /** Plug health status */ + public get status(): Health.Status { + // Implementation + } + /** Plug health checks */ + public get checks(): Health.Checks { + // Implementation + } +} +``` + +#### **Plugs.Sink.Tap** + +```typescript +import { Plugs, JobRequest } from '@mdf.js/firehose'; +import { EventEmitter } from 'events'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +/** Class that implements the Plugs.Sink.Tap interface */ +class MySinkPlug extends EventEmitter implements Plugs.Sink.Tap { + /** Constructor */ + constructor() { + super(); + } + /** Method that is called to process a single job */ + public single(job: JobObject): Promise { + // Implementation + } + /** Start the plug and the underlying provider */ + public async start(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider */ + public async stop(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider and clean the resources */ + public async close(): Promise { + // Implementation + } + /** Prometheus registry to store the metrics of the plug */ + public get metrics(): Registry { + // Implementation + } + /** Plug health status */ + public get status(): Health.Status { + // Implementation + } + /** Plug health checks */ + public get checks(): Health.Checks { + // Implementation + } +} +``` + +#### **Plugs.Sink.Jet** + +```typescript +import { Plugs, JobRequest } from '@mdf.js/firehose'; +import { EventEmitter } from 'events'; +import { Registry } from 'prom-client'; +import { Health } from '@mdf.js/core'; + +/** Class that implements the Plugs.Sink.Jet interface */ +class MySinkPlug extends EventEmitter implements Plugs.Sink.Jet { + /** Constructor */ + constructor() { + super(); + } + /** Method that is called to process a single job */ + public single(job: JobObject): Promise { + // Implementation + } + /** Method that is called to process multiple jobs */ + public multi(jobs: JobObject[]): Promise { + // Implementation + } + /** Start the plug and the underlying provider */ + public async start(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider */ + public async stop(): Promise { + // Implementation + } + /** Stop the plug and the underlying provider and clean the resources */ + public async close(): Promise { + // Implementation + } + /** Prometheus registry to store the metrics of the plug */ + public get metrics(): Registry { + // Implementation + } + /** Plug health status */ + public get status(): Health.Status { + // Implementation + } + /** Plug health checks */ + public get checks(): Health.Checks { + // Implementation + } +} +``` + +### **Creating a Strategy** + +Strategies provide customizable, `type`-based functions that define how to transform job-carried data. Strategies can filter, transform, enrich, or aggregate data as required. They can be chained to build complex data processing workflows, allowing developers to create tailored data pipelines. + +To create a strategy, you need to create a class that implements the `Strategy` interface. The `Strategy` interface includes the following methods and properties: + +- `do(process: JobHandler): Promise`: Method that is called to process a job. The parameter is the `JobHandler` to process. This method must return a promise that resolves to a `JobHandler` with the processed data. +- `name: string`: Strategy name, used to identify the strategy in the pipeline. + +```typescript +import { Jobs } from '@mdf.js/core'; + +/** Class that implements the Strategy interface */ +class MyStrategy implements Jobs.Strategy { + /** Strategy name */ + public get name(): string { + return 'MyStrategy'; + } + /** Method that is called to process a job */ + public async do(process: Jobs.JobHandler): Promise { + // Process the job + return process; + } +} +``` + +### **Event Handling** + +The `Firehose` class extends `EventEmitter` and emits several events that you can listen to: + +- `error`: Emitted when the component detects an error. + + ```typescript + firehose.on('error', (error) => { + console.error('An error occurred:', error); + }); + ``` + +- `status`: Emitted when the component's status changes. + + ```typescript + firehose.on('status', (status) => { + console.log('Firehose status:', status); + }); + ``` + +- `job`: Emitted when a new job is received from a source. + + ```typescript + firehose.on('job', (job) => { + console.log('New job received:', job); + }); + ``` + +- `done`: Emitted when a job has ended, either due to completion or failure. + + ```typescript + firehose.on('done', (uuid, result, error) => { + if (error) { + console.error(`Job ${uuid} failed with error:`, error); + } else { + console.log(`Job ${uuid} completed with result:`, result); + } + }); + ``` + +- `hold`: Emitted when the engine is paused due to inactivity. + + ```typescript + firehose.on('hold', () => { + console.warn('Firehose is on hold due to inactivity.'); + }); + ``` + +### **Firehose Configuration and Lifecycle** + +To instantiate a `Firehose`, you need to provide a name and options that include the sources, sinks, and optionally, strategies, retry options, buffer size, etc. + +```typescript +import { Firehose } from '@mdf.js/firehose'; + +const firehose = new Firehose('MyFirehose', { + sources: [/* Your source plugs */], + sinks: [/* Your sink plugs */], + strategies: { + TypeOne: [/* Strategies for TypeOne */], + TypeTwo: [/* Strategies for TypeTwo */], + }, + retryOptions: /* Your retry options */, + bufferSize: 100, + atLeastOne: true, + logger: /* Your logger instance */, + maxInactivityTime: 60000, +}); +``` + +To manage the lifecycle of the `Firehose`, you can use the following methods: + +- `start(): Promise`: Starts the firehose, initializing all the sources and sinks, and begins processing jobs. +- `stop(): Promise`: Stops the firehose, gracefully shutting down all the sources and sinks. +- `close(): Promise`: Stops the firehose and cleans up all resources. +- `restart(): Promise`: Restarts the firehose, re-initializing all components. + +Example: + +```typescript +await firehose.start(); +// Firehose is now running + +// Later, when you need to stop it +await firehose.stop(); +``` + +### **Metrics and Health Checks** + +The `Firehose` class includes a Prometheus registry to store the metrics of the pipeline. As you can see in the previous examples, new metrics can be added to the plug classes. The `Firehose` instance will collect all the metrics of the plugs and will expose them in the `metrics` property of the `Firehose` instance. + +The default metrics included in the `Firehose` instance are: + +- `api_all_job_processed_total`: The total number of all jobs processed, with the label `type`. +- `api_all_errors_job_processing_total`: The total number of errors processing jobs, with the label `type`. +- `api_all_job_in_processing_total`: Number of jobs currently processing (no response yet), with the label `type`. +- `api_publishing_job_duration_milliseconds`: Firehose jobs duration, with the label `type`. +- `api_publishing_throughput`: Firehose throughput in bytes, with the label `type`. + +On the other side, `Plugs` should include health information in the `checks` and `status` properties. This information will be grouped with the `Firehose` health information. The `status` property should be `pass` if all the checks are passing and `fail` if any check is failing. + +The health information can be classified into two types: stream status (number of pending jobs in the pipeline) and recent plug operations. + +You can access the health status and checks through the `status` and `checks` properties: + +```typescript +const firehoseStatus = firehose.status; +const firehoseChecks = firehose.checks; +``` + ## **License** Copyright 2024 Mytra Control S.L. All rights reserved. diff --git a/packages/api/firehose/media/firehose-diagram.svg b/packages/api/firehose/media/firehose-diagram.svg new file mode 100644 index 00000000..a8f19faf --- /dev/null +++ b/packages/api/firehose/media/firehose-diagram.svg @@ -0,0 +1,3 @@ + + +

                          @mdf.js/firehose Module

                          Writable Stream

                          Writable Stream

                          Transform Stream
                          (Strategy)

                          Readable Stream

                          Readable Stream

                          Readable Stream

                          Data Flow

                          Data Flow

                          Data Flow

                          Pipe

                          Pipe

                          Pipe

                          Pipe

                          Pipe

                          Processed Data

                          Processed Data

                          Source.Plug.
                          Flow

                          Source.Plug.
                          Sequence

                          Source.Plug.
                          CreditFlow

                          Data Processing &
                          Transformation

                          Sink.Plug.
                          Tap

                          Sink.Plug.
                          Jet

                          Database or
                          Data Storage

                          Message Broker

                          Data Warehouse

                          Analytics Platform

                          \ No newline at end of file diff --git a/packages/api/firehose/package.json b/packages/api/firehose/package.json index 848776f7..44524eff 100644 --- a/packages/api/firehose/package.json +++ b/packages/api/firehose/package.json @@ -1,53 +1,52 @@ -{ - "name": "@mdf.js/firehose", - "version": "0.0.1", - "description": "MMS - API Core - Firehose library", - "keywords": [ - "NodeJS", - "MMS", - "API", - "firehose" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/firehose" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/firehose/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "lodash": "^4.17.21", - "prom-client": "^15.1.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/firehose", + "version": "0.0.1", + "description": "MMS - API Core - Firehose library", + "keywords": [ + "NodeJS", + "MMS", + "API", + "firehose" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/firehose" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/firehose/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "lodash": "^4.17.21", + "prom-client": "^15.1.3", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/firehose/src/Engine/Engine.ts b/packages/api/firehose/src/Engine/Engine.ts index 4a0dc863..9ab9ac88 100644 --- a/packages/api/firehose/src/Engine/Engine.ts +++ b/packages/api/firehose/src/Engine/Engine.ts @@ -109,7 +109,7 @@ export class Engine extends Transform implements Layer.App.Component { private executeStrategy(job: OpenJobHandler, strategy: OpenStrategy): OpenJobHandler { try { const result = strategy.do(job.toObject()); - if (!result || result.data === undefined || result.data === null) { + if (result?.data == null) { job.addError( new Crash( `Strategy ${strategy.name} return an undefined job or a job with no data, it has not be applied`, diff --git a/packages/api/firehose/src/Firehose.ts b/packages/api/firehose/src/Firehose.ts index c00e7e9b..6c20c115 100644 --- a/packages/api/firehose/src/Firehose.ts +++ b/packages/api/firehose/src/Firehose.ts @@ -15,15 +15,7 @@ import { v4 } from 'uuid'; import { Engine } from './Engine'; import { Helpers } from './helpers'; import { MetricsHandler } from './metrics'; -import { FirehoseOptions, Sinks, Sources } from './types'; - -/** Firehose `job` event handler */ -export type JobEventHandler< - Type extends string = string, - Data = any, - CustomHeaders extends Record = Jobs.NoMoreHeaders, - CustomOptions extends Record = Jobs.NoMoreOptions, -> = (job: Jobs.JobObject) => void; +import { FirehoseOptions, JobEventHandler, Sinks, Sources } from './types'; export declare interface Firehose< Type extends string = string, @@ -49,7 +41,7 @@ export declare interface Firehose< * Register an event listener over the `job` event, which is emitted when a new job is received * from a source. * @param event - `job` event - * @param job - Job object + * @param listener - Job event listener * @event */ on(event: 'job', listener: JobEventHandler): this; @@ -57,9 +49,7 @@ export declare interface Firehose< * Register an event listener over the `done` event, which is emitted when a job has ended, either * due to completion or failure. * @param event - `done` event - * @param uuid - Unique job processing identification - * @param result - Job {@link Result} - * @param error - Error raised during job processing, if any + * @param listener - Done event listener * @event */ on(event: 'done', listener: Jobs.DoneEventHandler): this; @@ -67,6 +57,7 @@ export declare interface Firehose< * Register an event listener over the `hold` event, which is emitted when the engine is paused due * to inactivity. * @param event - `restart` event + * @param listener - Hold event listener * @event */ on(event: 'hold', listener: () => void): this; @@ -74,9 +65,7 @@ export declare interface Firehose< * Register an event listener over the `done` event, which is emitted when a job has ended, either * due to completion or failure. * @param event - `done` event - * @param uuid - Unique job processing identification - * @param result - Job {@link Result} - * @param error - Error raised during job processing, if any + * @param listener - Done event listener * @event */ addListener(event: 'done', listener: Jobs.DoneEventHandler): this; @@ -84,9 +73,7 @@ export declare interface Firehose< * Registers a event listener over the `done` event, at the beginning of the listeners array, * which is emitted when a job has ended, either due to completion or failure. * @param event - `done` event - * @param uuid - Unique job processing identification - * @param result - Job {@link Result} - * @param error - Error raised during job processing, if any + * @param listener - Done event listener * @event */ prependListener(event: 'done', listener: Jobs.DoneEventHandler): this; @@ -94,9 +81,7 @@ export declare interface Firehose< * Registers a one-time event listener over the `done` event, which is emitted when a job has * ended, either due to completion or failure. * @param event - `done` event - * @param uuid - Unique job processing identification - * @param result - Job {@link Result} - * @param error - Error raised during job processing, if any + * @param listener - Done event listener * @event */ once(event: 'done', listener: Jobs.DoneEventHandler): this; @@ -104,9 +89,7 @@ export declare interface Firehose< * Registers a one-time event listener over the `done` event, at the beginning of the listeners * array, which is emitted when a job has ended, either due to completion or failure. * @param event - `done` event - * @param uuid - Unique job processing identification - * @param result - Job {@link Result} - * @param error - Error raised during job processing, if any + * @param listener - Done event listener * @event */ prependOnceListener(event: 'done', listener: Jobs.DoneEventHandler): this; diff --git a/packages/api/firehose/src/Sink/core/PlugWrapper.ts b/packages/api/firehose/src/Sink/core/PlugWrapper.ts index 40688af1..9b7e367c 100644 --- a/packages/api/firehose/src/Sink/core/PlugWrapper.ts +++ b/packages/api/firehose/src/Sink/core/PlugWrapper.ts @@ -1,159 +1,159 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { Crash, Multi } from '@mdf.js/crash'; -import { RetryOptions, retryBind } from '@mdf.js/utils'; -import EventEmitter from 'events'; -import { merge } from 'lodash'; -import { Registry } from 'prom-client'; -import { OpenJobObject, WrappableSinkPlug } from '../../types'; - -export class PlugWrapper extends EventEmitter implements Layer.App.Component { - /** Indicate if the last operation was finished with error */ - private lastOperationError?: Crash | Multi; - /** Date of the last operation performed */ - private lastOperationDate?: Date; - /** Operation retry options */ - private readonly retryOptions: RetryOptions; - /** Plug single operation original */ - private readonly singleOriginal: (job: OpenJobObject) => Promise; - /** Plug multi operation original */ - private readonly multiOriginal?: (jobs: OpenJobObject[]) => Promise; - /** Plug start operation original */ - private readonly startOriginal: () => Promise; - /** Plug stop operation original */ - private readonly stopOriginal: () => Promise; - /** - * Create a new instance of PlugWrapper - * @param plug - sink plug instance - * @param retryOptions - options for job retry operations - */ - constructor( - private readonly plug: WrappableSinkPlug, - retryOptions?: RetryOptions - ) { - super(); - this.retryOptions = merge({ logger: this.onOperationError }, retryOptions); - if (!this.plug) { - throw new Crash('PlugWrapper requires a plug instance'); - } else if (!this.plug.single) { - throw new Crash(`Plug ${this.plug.name} does not implement the single method`); - } else if (typeof this.plug.single !== 'function') { - throw new Crash(`Plug ${this.plug.name} does not implement the single method properly`); - } else { - this.singleOriginal = this.plug.single; - this.plug.single = this.single; - } - if (this.plug.multi) { - if (typeof this.plug.multi !== 'function') { - throw new Crash(`Plug ${this.plug.name} not implement the multi method properly`); - } else { - this.multiOriginal = this.plug.multi; - this.plug.multi = this.multi; - } - } - if (typeof this.plug.start !== 'function') { - throw new Crash(`Plug ${this.plug.name} not implement the start method properly`); - } else { - this.startOriginal = this.plug.start; - this.plug.start = this.start; - } - if (typeof this.plug.stop !== 'function') { - throw new Crash(`Plug ${this.plug.name} not implement the stop method properly`); - } else { - this.stopOriginal = this.plug.stop; - this.plug.stop = this.stop; - } - } - /** Register an error in the plug operation */ - private readonly onOperationError = (rawError: Crash | Multi): void => { - this.lastOperationError = Crash.from(rawError, this.plug.componentId); - this.lastOperationDate = new Date(); - if (this.listenerCount('error') > 0) { - this.emit('error', this.lastOperationError); - } - }; - /** Register an error in the plug operation */ - private readonly onOperationSuccess = (): void => { - this.lastOperationError = undefined; - this.lastOperationDate = new Date(); - }; - /** - * Perform the retry functionality for a promise - * @param task - promise to execute - * @param funcArgs - promise arguments - * @param options - control execution options - * @returns - */ - private async wrappedOperation( - task: (...args: any[]) => Promise, - funcArgs: any[] - ): Promise { - const result = await retryBind(task, this.plug, funcArgs, this.retryOptions); - this.onOperationSuccess(); - return result; - } - /** - * Perform the processing of a single Job - * @param job - job to be processed - */ - private readonly single = async (job: OpenJobObject): Promise => { - await this.wrappedOperation(this.singleOriginal, [job]); - }; - /** - * Perform the processing of several Jobs - * @param jobs - jobs to be processed - */ - private readonly multi = async (jobs: OpenJobObject[]): Promise => { - if (!this.multiOriginal) { - throw new Crash(`Plug ${this.plug.name} does not implement the multi method`); - } - await this.wrappedOperation(this.multiOriginal, [jobs]); - }; - /** Start the Plug and the underlayer resources, making it available */ - private readonly start = async (): Promise => { - await this.wrappedOperation(this.startOriginal, []); - }; - /** Stop the Plug and the underlayer resources, making it unavailable */ - private readonly stop = async (): Promise => { - await this.wrappedOperation(this.stopOriginal, []); - }; - /** Component name */ - public get name(): string { - return this.plug.name; - } - /** Component identification */ - public get componentId(): string { - return this.plug.componentId; - } - /** Metrics registry for this component */ - public get metrics(): Registry | undefined { - return this.plug.metrics; - } - /** - * Return the status of the stream in a standard format - * @returns _check object_ as defined in the draft standard - * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 - */ - public get checks(): Health.Checks { - return { - ...this.plug.checks, - [`${this.plug.name}:lastOperation`]: [ - { - status: this.lastOperationError ? 'fail' : 'pass', - componentId: this.plug.componentId, - componentType: 'plug', - observedValue: this.lastOperationError ? 'error' : 'ok', - observedUnit: 'result of last operation', - time: this.lastOperationDate ? this.lastOperationDate.toISOString() : undefined, - output: this.lastOperationError ? this.lastOperationError.trace() : undefined, - }, - ], - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { Crash, Multi } from '@mdf.js/crash'; +import { RetryOptions, retryBind } from '@mdf.js/utils'; +import EventEmitter from 'events'; +import { merge } from 'lodash'; +import { Registry } from 'prom-client'; +import { OpenJobObject, WrappableSinkPlug } from '../../types'; + +export class PlugWrapper extends EventEmitter implements Layer.App.Component { + /** Indicate if the last operation was finished with error */ + private lastOperationError?: Crash | Multi; + /** Date of the last operation performed */ + private lastOperationDate?: Date; + /** Operation retry options */ + private readonly retryOptions: RetryOptions; + /** Plug single operation original */ + private readonly singleOriginal: (job: OpenJobObject) => Promise; + /** Plug multi operation original */ + private readonly multiOriginal?: (jobs: OpenJobObject[]) => Promise; + /** Plug start operation original */ + private readonly startOriginal: () => Promise; + /** Plug stop operation original */ + private readonly stopOriginal: () => Promise; + /** + * Create a new instance of PlugWrapper + * @param plug - sink plug instance + * @param retryOptions - options for job retry operations + */ + constructor( + private readonly plug: WrappableSinkPlug, + retryOptions?: RetryOptions + ) { + super(); + this.retryOptions = merge({ logger: this.onOperationError }, retryOptions); + if (!this.plug) { + throw new Crash('PlugWrapper requires a plug instance'); + } else if (!this.plug.single) { + throw new Crash(`Plug ${this.plug.name} does not implement the single method`); + } else if (typeof this.plug.single !== 'function') { + throw new Crash(`Plug ${this.plug.name} does not implement the single method properly`); + } else { + this.singleOriginal = this.plug.single; + this.plug.single = this.single; + } + if (this.plug.multi) { + if (typeof this.plug.multi !== 'function') { + throw new Crash(`Plug ${this.plug.name} not implement the multi method properly`); + } else { + this.multiOriginal = this.plug.multi; + this.plug.multi = this.multi; + } + } + if (typeof this.plug.start !== 'function') { + throw new Crash(`Plug ${this.plug.name} not implement the start method properly`); + } else { + this.startOriginal = this.plug.start; + this.plug.start = this.start; + } + if (typeof this.plug.stop !== 'function') { + throw new Crash(`Plug ${this.plug.name} not implement the stop method properly`); + } else { + this.stopOriginal = this.plug.stop; + this.plug.stop = this.stop; + } + } + /** Register an error in the plug operation */ + private readonly onOperationError = (rawError: Crash | Multi): void => { + this.lastOperationError = Crash.from(rawError, this.plug.componentId); + this.lastOperationDate = new Date(); + if (this.listenerCount('error') > 0) { + this.emit('error', this.lastOperationError); + } + }; + /** Register an error in the plug operation */ + private readonly onOperationSuccess = (): void => { + this.lastOperationError = undefined; + this.lastOperationDate = new Date(); + }; + /** + * Perform the retry functionality for a promise + * @param task - promise to execute + * @param funcArgs - promise arguments + * @param options - control execution options + * @returns promise result + */ + private async wrappedOperation( + task: (...args: any[]) => Promise, + funcArgs: any[] + ): Promise { + const result = await retryBind(task, this.plug, funcArgs, this.retryOptions); + this.onOperationSuccess(); + return result; + } + /** + * Perform the processing of a single Job + * @param job - job to be processed + */ + private readonly single = async (job: OpenJobObject): Promise => { + await this.wrappedOperation(this.singleOriginal, [job]); + }; + /** + * Perform the processing of several Jobs + * @param jobs - jobs to be processed + */ + private readonly multi = async (jobs: OpenJobObject[]): Promise => { + if (!this.multiOriginal) { + throw new Crash(`Plug ${this.plug.name} does not implement the multi method`); + } + await this.wrappedOperation(this.multiOriginal, [jobs]); + }; + /** Start the Plug and the underlayer resources, making it available */ + private readonly start = async (): Promise => { + await this.wrappedOperation(this.startOriginal, []); + }; + /** Stop the Plug and the underlayer resources, making it unavailable */ + private readonly stop = async (): Promise => { + await this.wrappedOperation(this.stopOriginal, []); + }; + /** Component name */ + public get name(): string { + return this.plug.name; + } + /** Component identification */ + public get componentId(): string { + return this.plug.componentId; + } + /** Metrics registry for this component */ + public get metrics(): Registry | undefined { + return this.plug.metrics; + } + /** + * Return the status of the stream in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + return { + ...this.plug.checks, + [`${this.plug.name}:lastOperation`]: [ + { + status: this.lastOperationError ? 'fail' : 'pass', + componentId: this.plug.componentId, + componentType: 'plug', + observedValue: this.lastOperationError ? 'error' : 'ok', + observedUnit: 'result of last operation', + time: this.lastOperationDate ? this.lastOperationDate.toISOString() : undefined, + output: this.lastOperationError ? this.lastOperationError.trace() : undefined, + }, + ], + }; + } +} diff --git a/packages/api/firehose/src/index.ts b/packages/api/firehose/src/index.ts index dbee5966..bf825eff 100644 --- a/packages/api/firehose/src/index.ts +++ b/packages/api/firehose/src/index.ts @@ -1,12 +1,12 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Health, Jobs } from '@mdf.js/core'; -export { LoggerInstance } from '@mdf.js/logger'; -export { RetryOptions } from '@mdf.js/utils'; -export * from './Firehose'; -export { FirehoseOptions, Plugs, PostConsumeOptions } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Health, Jobs } from '@mdf.js/core'; +export { LoggerInstance } from '@mdf.js/logger'; +export { RetryOptions } from '@mdf.js/utils'; +export * from './Firehose'; +export { FirehoseOptions, JobEventHandler, Plugs, PostConsumeOptions } from './types'; diff --git a/packages/api/firehose/src/metrics/MetricsHandler.ts b/packages/api/firehose/src/metrics/MetricsHandler.ts index a3c22579..597e38dd 100644 --- a/packages/api/firehose/src/metrics/MetricsHandler.ts +++ b/packages/api/firehose/src/metrics/MetricsHandler.ts @@ -1,121 +1,122 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Jobs } from '@mdf.js/core'; -import { Counter, Gauge, Histogram, Registry } from 'prom-client'; -import { Sinks, Sources } from '../types'; - -/** Metric types */ -type MetricInstances = { - /** The total number of all jobs processed */ - jobsProcessed: Counter; - /** The total number errors processing jobs */ - jobsWithError: Counter; - /** Number of jobs actually processing */ - jobsInProcess: Gauge; - /** Firehose jobs duration */ - jobsDuration: Histogram; - /** Firehose throughput in bytes */ - jobsThroughput: Histogram; -}; - -/** Metrics handler */ -export class MetricsHandler { - /** Map of registered plugs */ - private plugs: Map = new Map(); - /** Metrics instances */ - private readonly metrics: MetricInstances; - /** The registry to register the metrics */ - public registry: Registry; - /** Create an instance of MetricsHandler */ - constructor() { - this.registry = new Registry(); - this.registry.resetMetrics(); - this.metrics = this.defineMetrics(this.registry); - } - /** - * Update the job processing metrics of a firehose - * @param job - job to be managed - */ - private readonly onJobEventHandler = (job: Jobs.JobHandler): void => { - this.metrics.jobsInProcess.inc({ type: job.type }); - const size = Buffer.from(JSON.stringify(job.data), 'utf-8').byteLength; - const onDoneHandler: (uuid: string, result: Jobs.Result) => void = ( - uuid: string, - result: Jobs.Result - ) => { - this.metrics.jobsInProcess.dec({ type: job.type }); - if (result.hasErrors) { - this.metrics.jobsWithError.inc({ type: job.type }); - } else { - this.metrics.jobsProcessed.inc({ type: job.type }); - } - const duration = job.processTime; - this.metrics.jobsDuration.observe({ type: job.type }, duration); - this.metrics.jobsThroughput.observe({ type: job.type }, size); - }; - job.once('done', onDoneHandler); - }; - /** - * Define the metrics over a registry - * @param register - The registry to register the metrics - * @returns The metric instances - */ - private defineMetrics(register: Registry): MetricInstances { - return { - jobsProcessed: new Counter({ - name: 'api_all_job_processed_total', - help: 'The total number of all jobs processed', - labelNames: ['type'], - registers: [register], - }), - jobsWithError: new Counter({ - name: 'api_all_errors_job_processing_total', - help: 'The total number errors processing jobs', - labelNames: ['type'], - registers: [register], - }), - jobsInProcess: new Gauge({ - name: 'api_all_job_in_processing_total', - help: 'Number of jobs actually processing (no response yet)', - labelNames: ['type'], - registers: [register], - }), - jobsDuration: new Histogram({ - name: 'api_publishing_job_duration_milliseconds', - help: 'Firehose jobs duration', - buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], - labelNames: ['type'], - registers: [register], - }), - jobsThroughput: new Histogram({ - name: 'api_publishing_throughput', - help: 'Firehose throughput in bytes', - buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], - labelNames: ['type'], - registers: [register], - }), - }; - } - /** - * Register the metrics handler to a firehose source - * @param plug - Source to be managed - */ - public enroll(plug: Sources | Sinks): void { - plug.on('job', this.onJobEventHandler); - if (!this.plugs.has(plug.name)) { - if ( - 'metrics' in plug && - typeof plug.metrics !== 'undefined' && - plug.metrics instanceof Registry - ) { - this.registry = Registry.merge([this.registry, plug.metrics]); - this.plugs.set(plug.name, plug); - } - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Jobs } from '@mdf.js/core'; +import { Counter, Gauge, Histogram, Registry } from 'prom-client'; +import { Sinks, Sources } from '../types'; + +/** Metric types */ +type MetricInstances = { + /** The total number of all jobs processed */ + jobsProcessed: Counter; + /** The total number errors processing jobs */ + jobsWithError: Counter; + /** Number of jobs actually processing */ + jobsInProcess: Gauge; + /** Firehose jobs duration */ + jobsDuration: Histogram; + /** Firehose throughput in bytes */ + jobsThroughput: Histogram; +}; + +/** Metrics handler */ +export class MetricsHandler { + /** Map of registered plugs */ + private readonly plugs: Map = new Map(); + /** Metrics instances */ + private readonly metrics: MetricInstances; + /** The registry to register the metrics */ + public registry: Registry; + /** Create an instance of MetricsHandler */ + constructor() { + this.registry = new Registry(); + this.registry.resetMetrics(); + this.metrics = this.defineMetrics(this.registry); + } + /** + * Update the job processing metrics of a firehose + * @param job - job to be managed + */ + private readonly onJobEventHandler = (job: Jobs.JobHandler): void => { + this.metrics.jobsInProcess.inc({ type: job.type }); + const size = Buffer.from(JSON.stringify(job.data), 'utf-8').byteLength; + const onDoneHandler: (uuid: string, result: Jobs.Result) => void = ( + uuid: string, + result: Jobs.Result + ) => { + this.metrics.jobsInProcess.dec({ type: job.type }); + if (result.hasErrors) { + this.metrics.jobsWithError.inc({ type: job.type }); + } else { + this.metrics.jobsProcessed.inc({ type: job.type }); + } + const duration = job.processTime; + this.metrics.jobsDuration.observe({ type: job.type }, duration); + this.metrics.jobsThroughput.observe({ type: job.type }, size); + }; + job.once('done', onDoneHandler); + }; + /** + * Define the metrics over a registry + * @param register - The registry to register the metrics + * @returns The metric instances + */ + private defineMetrics(register: Registry): MetricInstances { + return { + jobsProcessed: new Counter({ + name: 'api_all_job_processed_total', + help: 'The total number of all jobs processed', + labelNames: ['type'], + registers: [register], + }), + jobsWithError: new Counter({ + name: 'api_all_errors_job_processing_total', + help: 'The total number errors processing jobs', + labelNames: ['type'], + registers: [register], + }), + jobsInProcess: new Gauge({ + name: 'api_all_job_in_processing_total', + help: 'Number of jobs actually processing (no response yet)', + labelNames: ['type'], + registers: [register], + }), + jobsDuration: new Histogram({ + name: 'api_publishing_job_duration_milliseconds', + help: 'Firehose jobs duration', + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + labelNames: ['type'], + registers: [register], + }), + jobsThroughput: new Histogram({ + name: 'api_publishing_throughput', + help: 'Firehose throughput in bytes', + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + labelNames: ['type'], + registers: [register], + }), + }; + } + /** + * Register the metrics handler to a firehose source + * @param plug - Source to be managed + */ + public enroll(plug: Sources | Sinks): void { + plug.on('job', this.onJobEventHandler); + if (!this.plugs.has(plug.name)) { + if ( + 'metrics' in plug && + typeof plug.metrics !== 'undefined' && + plug.metrics instanceof Registry + ) { + this.registry = Registry.merge([this.registry, plug.metrics]); + this.plugs.set(plug.name, plug); + } + } + } +} + diff --git a/packages/api/firehose/src/types/JobEventHandler.t.ts b/packages/api/firehose/src/types/JobEventHandler.t.ts new file mode 100644 index 00000000..8c55f998 --- /dev/null +++ b/packages/api/firehose/src/types/JobEventHandler.t.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Jobs } from '@mdf.js/core'; + +/** Firehose `job` event handler */ +export type JobEventHandler< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.NoMoreHeaders, + CustomOptions extends Record = Jobs.NoMoreOptions, +> = (job: Jobs.JobObject) => void; diff --git a/packages/api/firehose/src/types/Plugs/Sink/Jet.i.ts b/packages/api/firehose/src/types/Plugs/Sink/Jet.i.ts index 8171ede3..0cb6f27b 100644 --- a/packages/api/firehose/src/types/Plugs/Sink/Jet.i.ts +++ b/packages/api/firehose/src/types/Plugs/Sink/Jet.i.ts @@ -1,21 +1,27 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Jobs } from '@mdf.js/core'; -import { Base } from './Base.i'; - -export interface Jet< - Type extends string = string, - Data = any, - CustomHeaders extends Record = Jobs.AnyHeaders, - CustomOptions extends Record = Jobs.AnyOptions, -> extends Base { - /** - * Perform the processing of several Jobs - * @param jobs - jobs to be processed - */ - multi: (jobs: Jobs.JobObject[]) => Promise; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Jobs } from '@mdf.js/core'; +import { Base } from './Base.i'; + +/** + * Jet Sink interface definition + * A Jet is a Sink that allows to manage multiple Jobs at a time using the `multi` method or one Job + * using the `single` method + */ +export interface Jet< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> extends Base { + /** + * Perform the processing of several Jobs + * @param jobs - jobs to be processed + */ + multi: (jobs: Jobs.JobObject[]) => Promise; +} + diff --git a/packages/api/firehose/src/types/Plugs/Sink/Tap.i.ts b/packages/api/firehose/src/types/Plugs/Sink/Tap.i.ts index 2a9b180a..affe2f01 100644 --- a/packages/api/firehose/src/types/Plugs/Sink/Tap.i.ts +++ b/packages/api/firehose/src/types/Plugs/Sink/Tap.i.ts @@ -1,16 +1,20 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Jobs } from '@mdf.js/core'; -import { Base } from './Base.i'; - -export type Tap< - Type extends string = string, - Data = any, - CustomHeaders extends Record = Jobs.AnyHeaders, - CustomOptions extends Record = Jobs.AnyOptions, -> = Base; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Jobs } from '@mdf.js/core'; +import { Base } from './Base.i'; + +/** + * Tap Sink interface definition + * A Tap is a Sink that only allows to manage one Job at a time using the `single` method + */ +export interface Tap< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> extends Base {} diff --git a/packages/api/firehose/src/types/Plugs/Source/CreditsFlow.i.ts b/packages/api/firehose/src/types/Plugs/Source/CreditsFlow.i.ts index 39329008..e48f7900 100644 --- a/packages/api/firehose/src/types/Plugs/Source/CreditsFlow.i.ts +++ b/packages/api/firehose/src/types/Plugs/Source/CreditsFlow.i.ts @@ -8,6 +8,11 @@ import { Jobs } from '@mdf.js/core'; import { Base } from './Base.i'; +/** + * CreditsFlow Source interface definition + * A CreditsFlow is a Source that allows to manage the flow of Jobs using a credit system to control + * the rate of Jobs that can be processed + */ export interface CreditsFlow< Type extends string = string, Data = any, @@ -20,4 +25,3 @@ export interface CreditsFlow< */ addCredits(credits: number): Promise; } - diff --git a/packages/api/firehose/src/types/Plugs/Source/Flow.i.ts b/packages/api/firehose/src/types/Plugs/Source/Flow.i.ts index 4eeee291..dac143e4 100644 --- a/packages/api/firehose/src/types/Plugs/Source/Flow.i.ts +++ b/packages/api/firehose/src/types/Plugs/Source/Flow.i.ts @@ -1,21 +1,27 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Jobs } from '@mdf.js/core'; -import { Base } from './Base.i'; - -export interface Flow< - Type extends string = string, - Data = any, - CustomHeaders extends Record = Jobs.AnyHeaders, - CustomOptions extends Record = Jobs.AnyOptions, -> extends Base { - /** Enable consuming process */ - init(): void; - /** Stop consuming process */ - pause(): void; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Jobs } from '@mdf.js/core'; +import { Base } from './Base.i'; + +/** + * Flow Source interface definition + * A Flow is a Source that allows to manage the flow of Jobs using `init`/pause" methods to control + * the rate of Jobs that can be processed + */ +export interface Flow< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> extends Base { + /** Enable consuming process */ + init(): void; + /** Stop consuming process */ + pause(): void; +} + diff --git a/packages/api/firehose/src/types/Plugs/Source/Sequence.i.ts b/packages/api/firehose/src/types/Plugs/Source/Sequence.i.ts index 8b8ca3f5..878dc349 100644 --- a/packages/api/firehose/src/types/Plugs/Source/Sequence.i.ts +++ b/packages/api/firehose/src/types/Plugs/Source/Sequence.i.ts @@ -1,26 +1,32 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Jobs } from '@mdf.js/core'; -import { Base } from './Base.i'; -export interface Sequence< - Type extends string = string, - Data = any, - CustomHeaders extends Record = Jobs.AnyHeaders, - CustomOptions extends Record = Jobs.AnyOptions, -> extends Base { - /** - * Perform the ingestion of new jobs - * @param size - Number of jobs to be ingested - */ - ingestData( - size: number - ): Promise< - | Jobs.JobRequest - | Jobs.JobRequest[] - >; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Jobs } from '@mdf.js/core'; +import { Base } from './Base.i'; + +/** + * Sequence Source interface definition + * A Sequence is a Source that allows to manage the flow of Jobs using the `ingestData` method to control + * the rate of Jobs that can be processed + */ +export interface Sequence< + Type extends string = string, + Data = any, + CustomHeaders extends Record = Jobs.AnyHeaders, + CustomOptions extends Record = Jobs.AnyOptions, +> extends Base { + /** + * Perform the ingestion of new jobs + * @param size - Number of jobs to be ingested + */ + ingestData( + size: number + ): Promise< + | Jobs.JobRequest + | Jobs.JobRequest[] + >; +} diff --git a/packages/api/firehose/src/types/index.ts b/packages/api/firehose/src/types/index.ts index e9183168..4084905b 100644 --- a/packages/api/firehose/src/types/index.ts +++ b/packages/api/firehose/src/types/index.ts @@ -1,23 +1,24 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export * from './EngineOptions.i'; -export * from './FirehoseOptions.i'; -export * as Plugs from './Plugs'; -export * from './PostConsumeOptions.i'; -export * from './SinkOptions.i'; -export * from './SourceOptions.i'; -export * from './WrappableSinkPlug.i'; -export * from './WrappableSourcePlug.i'; - -import * as Sink from '../Sink'; -import * as Source from '../Source'; - -export * from './OpenJobs.t'; - -export type Sinks = Sink.Jet | Sink.Tap; -export type Sources = Source.Flow | Source.Sequence | Source.CreditsFlow; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './EngineOptions.i'; +export * from './FirehoseOptions.i'; +export * from './JobEventHandler.t'; +export * as Plugs from './Plugs'; +export * from './PostConsumeOptions.i'; +export * from './SinkOptions.i'; +export * from './SourceOptions.i'; +export * from './WrappableSinkPlug.i'; +export * from './WrappableSourcePlug.i'; + +import * as Sink from '../Sink'; +import * as Source from '../Source'; + +export * from './OpenJobs.t'; + +export type Sinks = Sink.Jet | Sink.Tap; +export type Sources = Source.Flow | Source.Sequence | Source.CreditsFlow; diff --git a/packages/api/logger/README.md b/packages/api/logger/README.md index cb225493..3ebdde84 100644 --- a/packages/api/logger/README.md +++ b/packages/api/logger/README.md @@ -1,8 +1,9 @@ -# **@mdf.js** +# **@mdf.js/logger** [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -12,8 +13,8 @@

                          -

                          Mytra Development Framework - @mdf.js

                          -
                          Typescript tools for development
                          +

                          @mdf.js/logger

                          +
                          Improved logger management for @mdf.js framework
                          @@ -21,21 +22,265 @@ ## **Table of contents** -- [**@mdf.js**](#mdfjs) +- [**@mdf.js/logger**](#mdfjslogger) - [**Table of contents**](#table-of-contents) - [**Introduction**](#introduction) - [**Installation**](#installation) - [**Information**](#information) - - [**Use**](#use) + - [**Features**](#features) + - [**Usage**](#usage) + - [**Creating a Logger Instance**](#creating-a-logger-instance) + - [**Logging Methods and Signatures**](#logging-methods-and-signatures) + - [**Function Parameters**](#function-parameters) + - [**Examples**](#examples) + - [**Contextual Logging**](#contextual-logging) + - [**Logging Errors and Crashes**](#logging-errors-and-crashes) + - [**Configuring the Logger**](#configuring-the-logger) + - [**Logger Configuration Interface**](#logger-configuration-interface) + - [**Console Transport Configuration**](#console-transport-configuration) + - [**File Transport Configuration**](#file-transport-configuration) + - [**Fluentd Transport Configuration**](#fluentd-transport-configuration) + - [**Log Levels**](#log-levels) + - [**Error Handling in Configuration**](#error-handling-in-configuration) + - [**Checking Logger State**](#checking-logger-state) - [**License**](#license) ## **Introduction** +`@mdf.js/logger` is a powerful and flexible logging module designed for the `@mdf.js` framework. It provides enhanced logging capabilities with support for multiple logging levels and transports, including console, file, and Fluentd. This module allows developers to easily integrate robust logging into their applications, enabling better debugging, monitoring, and error tracking. + +![Logger](./media/logging-capture.png) + ## **Installation** +Install the `@mdf.js/logger` module via npm: + +- **npm** + +```bash +npm install @mdf.js/logger +``` + +- **yarn** + +```bash +yarn add @mdf.js/logger +``` + ## **Information** -## **Use** +### **Features** + +- **Multiple Log Levels**: Supports standard log levels (`silly`, `debug`, `verbose`, `info`, `warn`, `error`) for granular control over logging output. +- **Customizable Transports**: Supports logging to console, files, and Fluentd. +- **Flexible Configuration**: Easily configure logging options to suit your application's needs. +- **Contextual Logging**: Support for context and unique identifiers (UUID) to trace logs across different parts of your application. +- **Error Handling**: Ability to log errors and crashes with detailed stack traces and metadata. +- **Integration with @mdf.js Framework**: Seamless integration with other `@mdf.js` modules. + +## **Usage** + +### **Creating a Logger Instance** + +To use `@mdf.js/logger` in your project, import the module and create a new logger instance: + +```typescript +import { Logger } from '@mdf.js/logger'; + +const logger = new Logger('my-app'); +``` + +This creates a new logger with default settings for your application labeled `'my-app'`. + +### **Logging Methods and Signatures** + +The logger instance provides methods for logging at different levels: + +- `silly(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `debug(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `verbose(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `info(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `warn(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `error(message: string, uuid?: string, context?: string, ...meta: any[]): void;` +- `crash(error: Crash | Boom | Multi, context?: string): void;` + +#### **Function Parameters** + +- **message**: A human-readable string message to log. +- **uuid** *(optional)*: A unique identifier (UUID) for tracing the log message across different components or requests. +- **context** *(optional)*: The context (e.g., class or function name) where the logger is logging. +- **...meta** *(optional)*: Additional metadata or objects to include in the log. + +#### **Examples** + +Logging an info message without UUID and context: + +```typescript +logger.info('Application started'); +``` + +Logging an error message with UUID and context: + +```typescript +const uuid = '02ef7b85-b88e-4134-b611-4056820cd689'; +const context = 'UserService'; + +logger.error('User not found', uuid, context, { userId: 'user123' }); +``` + +### **Contextual Logging** + +To simplify logging with a fixed context and UUID, you can create a wrapped logger using the `SetContext` function: + +```typescript +import { SetContext } from '@mdf.js/logger'; + +const wrappedLogger = SetContext(logger, 'AuthService', '123e4567-e89b-12d3-a456-426614174000'); + +wrappedLogger.info('User login successful', undefined, undefined, { userId: 'user123' }); +``` + +In this case, the `uuid` and `context` parameters are pre-set, and you can omit them in subsequent log calls. + +### **Logging Errors and Crashes** + +To log errors or crashes with detailed stack traces and metadata, use the `crash` method: + +```typescript +import { Crash } from '@mdf.js/crash'; + +try { + // Code that may throw an error +} catch (error) { + const crashError = Crash.from(error); + logger.crash(crashError, 'AuthService'); +} +``` + +The `crash` method logs the error at the `error` level, including the stack trace and additional information. + +### **Configuring the Logger** + +You can customize the logger by passing a configuration object: + +```typescript +import { Logger, LoggerConfig } from '@mdf.js/logger'; + +const config: LoggerConfig = { + console: { + enabled: true, + level: 'debug', + }, + file: { + enabled: true, + filename: 'logs/my-app.log', + level: 'info', + maxFiles: 5, + maxsize: 10485760, // 10 MB + zippedArchive: true, + }, + fluentd: { + enabled: false, + // Additional Fluentd configurations for fluent-logger module + }, +}; + +const logger = new Logger('my-app', config); +``` + +```` + +### **Using DebugLogger** + +If you prefer using the `debug` module, utilize the `DebugLogger` class: + +```typescript +import { DebugLogger } from '@mdf.js/logger'; + +const debugLogger = new DebugLogger('my-app'); + +debugLogger.debug('This is a debug message using DebugLogger'); +```` + +### **Logger Configuration Interface** + +The `LoggerConfig` interface allows you to configure different transports: + +```typescript +interface LoggerConfig { + console?: ConsoleTransportConfig; + file?: FileTransportConfig; + fluentd?: FluentdTransportConfig; +} +``` + +#### **Console Transport Configuration** + +```typescript +interface ConsoleTransportConfig { + enabled?: boolean; // Default: false + level?: LogLevel; // Default: 'info' +} +``` + +#### **File Transport Configuration** + +```typescript +interface FileTransportConfig { + enabled?: boolean; // Default: false + level?: LogLevel; // Default: 'info' + filename?: string; // Default: 'logs/mdf-app.log' + maxFiles?: number; // Default: 10 + maxsize?: number; // Default: 10485760 (10 MB) + zippedArchive?: boolean; // Default: false + json?: boolean; // Default: false +} +``` + +#### **Fluentd Transport Configuration** + +```typescript +type FluentdTransportConfig = { + enabled?: boolean; // Default: false + level?: LogLevel; // Default: 'info' + // Additional Fluentd-specific options here +}; +``` + +### **Log Levels** + +Available log levels are defined by the `LogLevel` type: + +```typescript +type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; +``` + +### **Error Handling in Configuration** + +The logger handles configuration errors gracefully. If there's an error in the provided configuration, the logger defaults to predefined settings and logs the configuration error: + +```typescript +const invalidConfig: LoggerConfig = { + console: { + enabled: true, + level: 'invalid-level' as LogLevel, // This will cause a validation error + }, +}; + +const logger = new Logger('my-app', invalidConfig); + +// The logger will use default settings and log the configuration error +``` + +### **Checking Logger State** + +You can check if the logger has encountered any configuration errors: + +```typescript +if (logger.hasError) { + console.error('Logger configuration error:', logger.configError); +} +``` ## **License** diff --git a/packages/api/logger/media/logging-capture.png b/packages/api/logger/media/logging-capture.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccdc9551f512bd21cf54582a915680dfd308f2c GIT binary patch literal 137218 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

                          uT5=Ea&#^+L1Qga4@_obqJ|qX*d1| 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

                          I%G;^pvIom8;B?g{mN=}ALt!ax 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 literal 0 HcmV?d00001 diff --git a/packages/api/logger/package.json b/packages/api/logger/package.json index e0720d57..4db2964c 100644 --- a/packages/api/logger/package.json +++ b/packages/api/logger/package.json @@ -1,54 +1,53 @@ -{ - "name": "@mdf.js/logger", - "version": "0.0.1", - "description": "MMS - API Logger - Enhanced logger library for mms artifacts", - "keywords": [ - "NodeJS", - "MMS", - "API", - "winston", - "logger" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/logger" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/logger/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/crash": "*", - "@mdf.js/utils": "*", - "debug": "^4.3.7", - "fluent-logger": "^3.4.1", - "joi": "^17.13.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0", - "winston": "^3.15.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/debug": "^4.1.8", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/logger", + "version": "0.0.1", + "description": "MMS - API Logger - Enhanced logger library for mms artifacts", + "keywords": [ + "NodeJS", + "MMS", + "API", + "winston", + "logger" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/logger" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/logger/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/crash": "*", + "@mdf.js/utils": "*", + "debug": "^4.4.0", + "fluent-logger": "^3.4.1", + "joi": "^17.13.3", + "tslib": "^2.8.1", + "uuid": "^11.0.3", + "winston": "^3.17.0" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/debug": "^4.1.8" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/logger/src/formats/formats.ts b/packages/api/logger/src/formats/formats.ts index e770268b..29cf7622 100644 --- a/packages/api/logger/src/formats/formats.ts +++ b/packages/api/logger/src/formats/formats.ts @@ -1,42 +1,54 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { format } from 'winston'; -const NULL_UUID = '00000000-0000-0000-0000-000000000000'; -const PID_PADDING = 8; -const LEVEL_PADDING = 7; -const LABEL_PADDING = 12; -const UUID_PADDING = 36; -const CONTEXT_PADDING = 12; - -const NULL_UUID_STR = NULL_UUID.padEnd(UUID_PADDING); -const UNKNOWN_CONTEXT_STR = 'unknown'.padEnd(CONTEXT_PADDING); -// ************************************************************************************************* -// #region Formateo de cadena de caracteres sin color -const customFormat = format.printf(info => { - let str = `${info['timestamp']} | `; - str += `${info['pid'].padEnd(PID_PADDING)} | `; - str += `${info['label'].padEnd(LABEL_PADDING)} | `; - str += `${info.level.padEnd(LEVEL_PADDING)} | `; - str += `${info['uuid'] || NULL_UUID_STR} | `; - str += `${ - info['context'] - ? info['context'].padEnd(CONTEXT_PADDING).substring(0, CONTEXT_PADDING) - : UNKNOWN_CONTEXT_STR - } | `; - str += info.message; - return str; -}); -// #endregion -// ************************************************************************************************* -// #region Formatos de registro -export function jsonFormat(label: string) { - return format.combine(format.label({ label }), format.splat(), format.json()); -} -export function stringFormat(label: string) { - return format.combine(format.label({ label }), format.splat(), format.simple(), customFormat); -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { format } from 'winston'; +const NULL_UUID = '00000000-0000-0000-0000-000000000000'; +const PID_PADDING = 8; +const LEVEL_PADDING = 7; +const LABEL_PADDING = 12; +const UUID_PADDING = 36; +const CONTEXT_PADDING = 12; + +const NULL_UUID_STR = NULL_UUID.padEnd(UUID_PADDING); +const UNKNOWN_CONTEXT_STR = 'unknown'.padEnd(CONTEXT_PADDING); + +/** + * Check if a value is a string. + * @param value - The value to check. + * @param defaultValue - The default value to return if the value is not a string. + * @returns The value as a string or the default value. + */ +function asString(value: unknown, defaultValue: string | number, padEnd: number): string { + const result = typeof value === 'string' ? value : `${defaultValue}`; + return result.padEnd(padEnd).substring(0, padEnd); +} +// ************************************************************************************************* +// #region Formateo de cadena de caracteres sin color +const customFormat = format.printf(info => { + const pid = asString(info['pid'], process.pid, PID_PADDING); + const label = asString(info['label'], 'unknown', LABEL_PADDING); + const uuid = asString(info['uuid'], NULL_UUID, UUID_PADDING); + const context = asString(info['context'], UNKNOWN_CONTEXT_STR, CONTEXT_PADDING); + + let str = `${info['timestamp']} | `; + str += `${pid} | `; + str += `${label} | `; + str += `${info.level.padEnd(LEVEL_PADDING)} | `; + str += `${uuid} | `; + str += `${context} | `; + str += info.message; + return str; +}); +// #endregion +// ************************************************************************************************* +// #region Formatos de registro +export function jsonFormat(label: string) { + return format.combine(format.label({ label }), format.splat(), format.json()); +} +export function stringFormat(label: string) { + return format.combine(format.label({ label }), format.splat(), format.simple(), customFormat); +} diff --git a/packages/api/logger/src/index.ts b/packages/api/logger/src/index.ts index 943e4723..2a18fedb 100644 --- a/packages/api/logger/src/index.ts +++ b/packages/api/logger/src/index.ts @@ -1,16 +1,16 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Logger } from './logger'; -const defaultLogger = new Logger(); -export default defaultLogger; -export { ConsoleTransportConfig } from './console'; -export { DebugLogger } from './debug'; -export { FileTransportConfig } from './file'; -export { FluentdTransportConfig } from './fluentd'; -export { Logger, LoggerConfig } from './logger'; -export { LoggerInstance } from './types'; -export { SetContext } from './wrapper'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Logger } from './logger'; +const defaultLogger = new Logger(); +export default defaultLogger; +export { ConsoleTransportConfig } from './console'; +export { DebugLogger } from './debug'; +export { FileTransportConfig } from './file'; +export { FluentdTransportConfig } from './fluentd'; +export { Logger, LoggerConfig } from './logger'; +export { LOG_LEVELS, LoggerFunction, LoggerInstance, LogLevel } from './types'; +export { SetContext, WrapperLogger } from './wrapper'; diff --git a/packages/api/logger/src/wrapper/Wrapper.test.ts b/packages/api/logger/src/wrapper/Wrapper.test.ts index ddd568e0..7d24b8af 100644 --- a/packages/api/logger/src/wrapper/Wrapper.test.ts +++ b/packages/api/logger/src/wrapper/Wrapper.test.ts @@ -1,61 +1,82 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Boom, Crash, Multi } from '@mdf.js/crash'; -import { LoggerInstance } from '../types'; -import { SetContext } from './Wrapper'; - -class MyLogger implements LoggerInstance { - value?: string; - silly = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - debug = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - verbose = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - info = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - warn = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - error = (msg: string, uuid?: string, context?: string) => { - this.value = msg + ': ' + uuid + ': ' + context; - }; - crash = (raw: Crash | Boom | Multi, context?: string) => { - this.value = raw.message + ': ' + context; - }; - stream = { write: (message: string) => {} }; -} - -describe('#Wrapper', () => { - describe('#Happy path', () => { - it('Should return the same message', () => { - const msg = 'My message'; - const uuid = 'uuid'; - const context = 'context'; - const myLogger = new MyLogger(); - const wrapper = SetContext(myLogger, context, uuid); - wrapper.silly(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.debug(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.verbose(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.info(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.warn(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.error(msg); - expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); - wrapper.crash(new Crash(msg)); - expect(myLogger.value).toEqual(msg + ': ' + context); - }); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Boom, Crash, Multi } from '@mdf.js/crash'; +import { LoggerInstance } from '../types'; +import { SetContext } from './Wrapper'; + +class MyLogger implements LoggerInstance { + value?: string; + silly = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + debug = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + verbose = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + info = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + warn = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + error = (msg: string, uuid?: string, context?: string) => { + this.value = msg + ': ' + uuid + ': ' + context; + }; + crash = (raw: Crash | Boom | Multi, context?: string) => { + this.value = raw.message + ': ' + context; + }; + stream = { write: (message: string) => {} }; +} + +describe('#Wrapper', () => { + describe('#Happy path', () => { + it('Should return the same message', () => { + const msg = 'My message'; + const uuid = 'uuid'; + const context = 'context'; + const myLogger = new MyLogger(); + const wrapper = SetContext(myLogger, context, uuid); + wrapper.silly(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.silly(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.debug(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.debug(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.verbose(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.verbose(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.info(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.info(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.warn(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.warn(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.error(msg); + expect(myLogger.value).toEqual(msg + ': ' + uuid + ': ' + context); + wrapper.error(msg, 'uuid2', 'context2'); + expect(myLogger.value).toEqual(msg + ': uuid2: context2'); + + wrapper.crash(new Crash(msg)); + expect(myLogger.value).toEqual(msg + ': ' + context); + wrapper.crash(new Crash(msg), 'context2'); + expect(myLogger.value).toEqual(msg + ': context2'); + }); + }); +}); + diff --git a/packages/api/logger/src/wrapper/Wrapper.ts b/packages/api/logger/src/wrapper/Wrapper.ts index 422c4411..47559c84 100644 --- a/packages/api/logger/src/wrapper/Wrapper.ts +++ b/packages/api/logger/src/wrapper/Wrapper.ts @@ -1,116 +1,116 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Boom, Crash, Multi } from '@mdf.js/crash'; -import { v4 } from 'uuid'; -import { LoggerFunction, LoggerInstance } from '../types'; - -class WrapperLogger { - /** - * Create a wrapped version of the logger where the context and uuid are already set - * @param logger - Logger instance to wrap - * @param context - context (class/function) where this logger is logging - * @param componentId - component identification - */ - public constructor( - private readonly logger: LoggerInstance, - private readonly context: string, - private readonly componentId: string = v4() - ) {} - /** - * Log events in the SILLY level: all the information in a very detailed way. - * This level used to be necessary only in the development process, and the meta data used to be - * the results of the operations. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - silly: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.silly(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the DEBUG level: all the information in a detailed way. - * This level used to be necessary only in the debugging process, so not all the data is - * reported, only the related with the main processes and tasks. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - debug: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.debug(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the VERBOSE level: trace information without details. - * This level used to be necessary only in system configuration process, so information about - * the settings and startup process used to be reported. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - verbose: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.verbose(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the INFO level: only relevant events are reported. - * This level is the default level. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - info: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.info(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the WARN level: information about possible problems or dangerous situations. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - warn: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.warn(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the ERROR level: all the errors and problems with detailed information. - * @param message - human readable information to log - * @param uuid - unique identifier for the actual job/task/request process - * @param context - context (class/function) where this logger is logging - * @param meta - extra information - */ - error: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { - this.logger.error(message, uuid || this.componentId, context || this.context, ...meta); - }; - /** - * Log events in the ERROR level: all the information in a very detailed way. - * This level used to be necessary only in the development process. - * @param rawError - crash error instance - * @param context - context (class/function) where this logger is logging - */ - crash = (rawError: Crash | Boom | Multi, context?: string) => { - this.logger.crash(rawError, context || this.context); - }; - /** Stream logger */ - stream: { write: (message: string) => void } = { - write: (message: string) => { - this.logger.stream.write(message); - }, - }; -} -/** - * Create a wrapped version of the logger where the context and uuid are already set - * @param logger - Logger instance to wrap - * @param context - context (class/function) where this logger is logging - * @param componentId - component identification - * @returns - */ -export function SetContext(logger: LoggerInstance, context: string, componentId?: string) { - return new WrapperLogger(logger, context, componentId); -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Boom, Crash, Multi } from '@mdf.js/crash'; +import { v4 } from 'uuid'; +import { LoggerFunction, LoggerInstance } from '../types'; + +export class WrapperLogger { + /** + * Create a wrapped version of the logger where the context and uuid are already set + * @param logger - Logger instance to wrap + * @param context - context (class/function) where this logger is logging + * @param componentId - component identification + */ + public constructor( + private readonly logger: LoggerInstance, + private readonly context: string, + private readonly componentId: string = v4() + ) {} + /** + * Log events in the SILLY level: all the information in a very detailed way. + * This level used to be necessary only in the development process, and the meta data used to be + * the results of the operations. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + silly: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.silly(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the DEBUG level: all the information in a detailed way. + * This level used to be necessary only in the debugging process, so not all the data is + * reported, only the related with the main processes and tasks. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + debug: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.debug(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the VERBOSE level: trace information without details. + * This level used to be necessary only in system configuration process, so information about + * the settings and startup process used to be reported. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + verbose: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.verbose(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the INFO level: only relevant events are reported. + * This level is the default level. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + info: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.info(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the WARN level: information about possible problems or dangerous situations. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + warn: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.warn(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the ERROR level: all the errors and problems with detailed information. + * @param message - human readable information to log + * @param uuid - unique identifier for the actual job/task/request process + * @param context - context (class/function) where this logger is logging + * @param meta - extra information + */ + error: LoggerFunction = (message: string, uuid?: string, context?: string, ...meta: any[]) => { + this.logger.error(message, uuid ?? this.componentId, context ?? this.context, ...meta); + }; + /** + * Log events in the ERROR level: all the information in a very detailed way. + * This level used to be necessary only in the development process. + * @param rawError - crash error instance + * @param context - context (class/function) where this logger is logging + */ + crash = (rawError: Crash | Boom | Multi, context?: string) => { + this.logger.crash(rawError, context ?? this.context); + }; + /** Stream logger */ + stream: { write: (message: string) => void } = { + write: (message: string) => { + this.logger.stream.write(message); + }, + }; +} +/** + * Create a wrapped version of the logger where the context and uuid are already set + * @param logger - Logger instance to wrap + * @param context - context (class/function) where this logger is logging + * @param componentId - component identification + * @returns + */ +export function SetContext(logger: LoggerInstance, context: string, componentId?: string) { + return new WrapperLogger(logger, context, componentId); +} diff --git a/packages/api/middlewares/README.md b/packages/api/middlewares/README.md index cb225493..6b4ba104 100644 --- a/packages/api/middlewares/README.md +++ b/packages/api/middlewares/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/middlewares/package.json b/packages/api/middlewares/package.json index a2eb423f..1fdaf012 100644 --- a/packages/api/middlewares/package.json +++ b/packages/api/middlewares/package.json @@ -32,13 +32,12 @@ "@mdf.js/crash": "*", "@mdf.js/logger": "*", "@mdf.js/redis-provider": "*", - "@mdf.js/utils": "*", "body-parser": "^1.20.2", "cors": "^2.8.5", - "express": "^4.21.1", - "express-rate-limit": "7.4.1", + "express": "^4.21.2", + "express-rate-limit": "7.5.0", "feature-policy": "^0.6.0", - "helmet": "^7.0.0", + "helmet": "^8.0.0", "http-errors": "^2.0.0", "joi": "^17.13.3", "jsonwebtoken": "^9.0.0", @@ -46,8 +45,8 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "prom-client": "^15.1.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" + "tslib": "^2.8.1", + "uuid": "^11.0.3" }, "devDependencies": { "@mdf.js/repo-config": "*", @@ -56,11 +55,10 @@ "@types/express": "^4.17.21", "@types/http-errors": "^2.0.1", "@types/jsonwebtoken": "^9.0.2", - "@types/lodash": "^4.17.10", + "@types/lodash": "^4.17.13", "@types/morgan": "^1.9.4", "@types/multer": "^1.4.12", "@types/supertest": "^6.0.2", - "@types/uuid": "^10.0.0", "supertest": "^7.0.0" }, "engines": { diff --git a/packages/api/middlewares/src/authz/authz.ts b/packages/api/middlewares/src/authz/authz.ts index 6117c12a..c2105119 100644 --- a/packages/api/middlewares/src/authz/authz.ts +++ b/packages/api/middlewares/src/authz/authz.ts @@ -1,157 +1,158 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Boom, BoomHelpers, Crash } from '@mdf.js/crash'; -import { NextFunction, Request, RequestHandler, Response } from 'express'; -import jwt, { Algorithm, JwtPayload } from 'jsonwebtoken'; -import { v4 } from 'uuid'; - -const DEFAULT_CONFIG_JWT_TOKEN_SECRET = v4(); -const DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS: Algorithm[] = ['HS256']; -const DEFAULT_CONFIG_JWT_ON_AUTHORIZATION = () => {}; - -type WithRequired = T & { [P in K]-?: T[P] }; - -/** Options for configuring authorization middleware */ -export type AuthZOptions = { - /** The secret used to sign and verify JWT tokens */ - secret?: string; - /** The algorithms used for JWT token verification */ - algorithms?: Algorithm[]; - /** The role(s) required for accessing the protected route */ - role?: string | string[]; - /** - * A callback function called when authorization is successful. - * @param decodedToken - The decoded JWT payload. - */ - onAuthorization?: (decodedToken: JwtPayload) => {}; -}; - -/** - * Check the token of the request - * @param options - authorization options - * @returns - */ -function authZ(options: AuthZOptions = {}): RequestHandler { - return (req: Request, res: Response, next: NextFunction) => { - try { - if (typeof options !== 'object' || Array.isArray(options)) { - throw new Crash(`Error in the authz middleware configuration`, req.uuid); - } - const onAuthorization = options.onAuthorization || DEFAULT_CONFIG_JWT_ON_AUTHORIZATION; - onAuthorization( - verify( - hasValidAuthenticationInformation(req.headers.authorization, req.uuid), - { - secret: options.secret || DEFAULT_CONFIG_JWT_TOKEN_SECRET, - algorithms: options.algorithms || DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS, - role: hasValidRole(options.role, req.uuid), - }, - req.uuid - ) - ); - next(); - } catch (error) { - next(error); - return; - } - }; -} -/** - * Check if the token is a valid token - * @param token - token to be verified - * @param uuid - request identifier - * @returns - */ -function hasValidAuthenticationInformation(token: string | undefined, uuid: string): string { - if (!token) { - throw BoomHelpers.badRequest(`No present authorization information`, uuid); - } else if (typeof token !== 'string') { - throw BoomHelpers.badRequest(`Malformed authorization information`, uuid); - } else { - const parts = token.split(' '); - if (parts.length !== 2) { - throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); - } - return parts[1]; - } -} -/** - * Check if a role is valid - * @param role - role to be checked - * @param uuid - request identifier - */ -function hasValidRole( - role: string | string[] | undefined, - uuid: string -): string | string[] | undefined { - if ( - !role || - typeof role === 'string' || - (Array.isArray(role) && role.length > 0 && role.every(item => typeof item === 'string')) - ) { - return role; - } else { - throw new Crash(`Error in the role authz middleware configuration`, uuid); - } -} -/** - * Verifies the JWT token and returns the decoded payload. - * - * @param token - The JWT token to verify. - * @param options - The options for JWT verification. - * @param uuid - The unique identifier for error tracking. - * @returns The decoded payload of the JWT token. - * @throws {Crash} If there is an error verifying the JWT token. - * @throws {BoomHelpers.BadRequest} If the JWT token is malformed. - * @throws {BoomHelpers.Unauthorized} If the user is not authorized. - * @throws {BoomHelpers.Unauthorized} If the JWT token is not valid. - */ -function verify( - token: string, - options: WithRequired, 'secret' | 'algorithms'>, - uuid: string -): JwtPayload { - try { - const decoded = jwt.verify(token, options.secret, { algorithms: options.algorithms }); - if (!decoded) { - throw new Crash(`Error verifying the JWT token`, uuid); - } - if (typeof decoded === 'string') { - throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); - } - if (typeof decoded['user'] !== 'string' || typeof decoded['role'] !== 'string') { - throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); - } - if (options.role) { - const allowedRoles = Array.isArray(options.role) ? options.role : [options.role]; - if (!allowedRoles.includes(decoded['role'])) { - throw BoomHelpers.unauthorized(`Not authorized`, uuid); - } - } - return decoded; - } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - throw BoomHelpers.unauthorized(`No valid token`, uuid); - } else if (error instanceof Boom) { - throw error; - } else { - throw Crash.from(error); - } - } -} - -/** AuthZ */ -export class AuthZ { - /** - * Perform the authorization based on jwt token for Socket.IO - * @param options - authorization options - * @returns - */ - public static handler(options?: AuthZOptions): RequestHandler { - return authZ(options); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Boom, BoomHelpers, Crash } from '@mdf.js/crash'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import jwt, { Algorithm, JwtPayload } from 'jsonwebtoken'; +import { v4 } from 'uuid'; + +const DEFAULT_CONFIG_JWT_TOKEN_SECRET = v4(); +const DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS: Algorithm[] = ['HS256']; +const DEFAULT_CONFIG_JWT_ON_AUTHORIZATION = () => {}; + +type WithRequired = T & { [P in K]-?: T[P] }; + +/** Options for configuring authorization middleware */ +export type AuthZOptions = { + /** The secret used to sign and verify JWT tokens */ + secret?: string; + /** The algorithms used for JWT token verification */ + algorithms?: Algorithm[]; + /** The role(s) required for accessing the protected route */ + role?: string | string[]; + /** + * A callback function called when authorization is successful. + * @param decodedToken - The decoded JWT payload. + */ + onAuthorization?: (decodedToken: JwtPayload) => {}; +}; + +/** + * Check the token of the request + * @param options - authorization options + * @returns + */ +function authZ(options: AuthZOptions = {}): RequestHandler { + return (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof options !== 'object' || Array.isArray(options)) { + throw new Crash(`Error in the authz middleware configuration`, req.uuid); + } + const onAuthorization = options.onAuthorization ?? DEFAULT_CONFIG_JWT_ON_AUTHORIZATION; + onAuthorization( + verify( + hasValidAuthenticationInformation(req.headers.authorization, req.uuid), + { + secret: options.secret ?? DEFAULT_CONFIG_JWT_TOKEN_SECRET, + algorithms: options.algorithms ?? DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS, + role: hasValidRole(options.role, req.uuid), + }, + req.uuid + ) + ); + next(); + } catch (error) { + next(error); + return; + } + }; +} +/** + * Check if the token is a valid token + * @param token - token to be verified + * @param uuid - request identifier + * @returns + */ +function hasValidAuthenticationInformation(token: string | undefined, uuid: string): string { + if (!token) { + throw BoomHelpers.badRequest(`No present authorization information`, uuid); + } else if (typeof token !== 'string') { + throw BoomHelpers.badRequest(`Malformed authorization information`, uuid); + } else { + const parts = token.split(' '); + if (parts.length !== 2) { + throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); + } + return parts[1]; + } +} +/** + * Check if a role is valid + * @param role - role to be checked + * @param uuid - request identifier + */ +function hasValidRole( + role: string | string[] | undefined, + uuid: string +): string | string[] | undefined { + if ( + !role || + typeof role === 'string' || + (Array.isArray(role) && role.length > 0 && role.every(item => typeof item === 'string')) + ) { + return role; + } else { + throw new Crash(`Error in the role authz middleware configuration`, uuid); + } +} +/** + * Verifies the JWT token and returns the decoded payload. + * + * @param token - The JWT token to verify. + * @param options - The options for JWT verification. + * @param uuid - The unique identifier for error tracking. + * @returns The decoded payload of the JWT token. + * @throws {Crash} If there is an error verifying the JWT token. + * @throws {BoomHelpers.BadRequest} If the JWT token is malformed. + * @throws {BoomHelpers.Unauthorized} If the user is not authorized. + * @throws {BoomHelpers.Unauthorized} If the JWT token is not valid. + */ +function verify( + token: string, + options: WithRequired, 'secret' | 'algorithms'>, + uuid: string +): JwtPayload { + try { + const decoded = jwt.verify(token, options.secret, { algorithms: options.algorithms }); + if (!decoded) { + throw new Crash(`Error verifying the JWT token`, uuid); + } + if (typeof decoded === 'string') { + throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); + } + if (typeof decoded['user'] !== 'string' || typeof decoded['role'] !== 'string') { + throw BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid); + } + if (options.role) { + const allowedRoles = Array.isArray(options.role) ? options.role : [options.role]; + if (!allowedRoles.includes(decoded['role'])) { + throw BoomHelpers.unauthorized(`Not authorized`, uuid); + } + } + return decoded; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + throw BoomHelpers.unauthorized(`No valid token`, uuid); + } else if (error instanceof Boom) { + throw error; + } else { + throw Crash.from(error); + } + } +} + +/** AuthZ */ +export class AuthZ { + /** + * Perform the authorization based on jwt token for Socket.IO + * @param options - authorization options + * @returns + */ + public static handler(options?: AuthZOptions): RequestHandler { + return authZ(options); + } +} + diff --git a/packages/api/middlewares/src/authz/index.ts b/packages/api/middlewares/src/authz/index.ts index 6b6b9b0d..32bcf27a 100644 --- a/packages/api/middlewares/src/authz/index.ts +++ b/packages/api/middlewares/src/authz/index.ts @@ -1,7 +1,7 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -export { AuthZ as AuthZ } from './authz'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +export { AuthZ, AuthZOptions } from './authz'; diff --git a/packages/api/middlewares/src/cache/Cache.ts b/packages/api/middlewares/src/cache/Cache.ts index 1c6801df..38c55902 100644 --- a/packages/api/middlewares/src/cache/Cache.ts +++ b/packages/api/middlewares/src/cache/Cache.ts @@ -1,229 +1,229 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import logger from '@mdf.js/logger'; -import { Redis } from '@mdf.js/redis-provider'; -import cryto from 'crypto'; -import { NextFunction, Request, RequestHandler, Response } from 'express'; -import { OutgoingHttpHeaders } from 'http'; -import { cloneDeep, merge } from 'lodash'; -import { CacheConfig } from './CacheConfig.i'; -import { CacheEntry } from './CacheEntry.t'; -import { CacheRepository } from './CacheRepository'; - -const DEFAULT_CONFIG_CACHE_DURATION = 60; -const DEFAULT_CONFIG_CACHE_ENABLED = true; -const DEFAULT_CONFIG_CACHE_HEADERS_BLACK_LIST: string[] = []; -const DEFAULT_CONFIG_STATUS_CODES_EXCLUDED: number[] = []; -const DEFAULT_CONFIG_STATUS_CODES_INCLUDED: number[] = [200]; -const DEFAULT_CONFIG_CACHE_PREFIX = 'api:cache:'; - -const DEFAULT_CONFIG_CACHE: CacheConfig = { - duration: DEFAULT_CONFIG_CACHE_DURATION, - enabled: DEFAULT_CONFIG_CACHE_ENABLED, - headersBlacklist: DEFAULT_CONFIG_CACHE_HEADERS_BLACK_LIST, - statusCodes: { - exclude: DEFAULT_CONFIG_STATUS_CODES_EXCLUDED, - include: DEFAULT_CONFIG_STATUS_CODES_INCLUDED, - }, - useBody: false, - prefixKey: DEFAULT_CONFIG_CACHE_PREFIX, - toggle: () => true, -}; - -/** CacheRequest middleware */ -export class Cache { - /** Middleware class name */ - private readonly context: string = this.constructor.name; - /** Cache repository */ - private readonly repository: CacheRepository; - /** Cache options */ - private readonly options: CacheConfig; - /** - * Cache middleware instance - * @param client - redis client - * @returns - */ - public static instance(client: Redis.Client, options?: Partial): Cache { - return new Cache(new CacheRepository(client), options); - } - /** - * Request cache middleware handler - * @param provider - redis client - * @param options - Cache options - * @returns - */ - public static handler(provider: Redis.Client, options?: Partial): RequestHandler { - return new Cache(new CacheRepository(provider), options).handler(options); - } - /** - * Create an instance of cache middleware - * @param options - cache configuration options - * @param repository - cache repository - */ - private constructor(repository: CacheRepository, options?: Partial) { - this.repository = repository; - this.options = merge(cloneDeep(DEFAULT_CONFIG_CACHE), options); - } - /** - * Cache middleware function - * @param options - audit function - * @returns - */ - public handler(options?: Partial): RequestHandler { - const localOptions = merge(cloneDeep(this.options), options); - return (req: Request, res: Response, next: NextFunction) => { - // Stryker disable next-line all - logger.debug(`New request for path [${req.url}]`, req.uuid, this.context); - if (req.headers['x-cache-bypass']) { - next(); - } else { - const cacheKey = this.cachePath(req, localOptions.prefixKey, localOptions.useBody); - this.repository - .getPath(cacheKey, req.uuid) - .then((result: CacheEntry | null) => { - if (result !== null) { - this.useCacheResponse(localOptions, result, req, res); - } else { - this.setCacheResponse(cacheKey, localOptions, req, res, next); - } - }) - .catch((error: Error | Crash) => { - // Stryker disable next-line all - logger.error(`Error storing in cache path ${req.url}: ${error.message}`); - next(); - }); - } - }; - } - /** - * Use the cached response to fullfil the request - * @param options - Cache options - * @param request - HTTP request express object - * @param response - HTTP response express object - */ - private useCacheResponse( - options: CacheConfig, - cachedResponse: CacheEntry, - request: Request, - response: Response - ): void { - // Stryker disable next-line all - logger.debug(`The response was cached`, request.uuid, this.context); - // Stryker disable next-line all - logger.silly(`${cachedResponse}`, request.uuid, this.context); - this.removeCacheHeaders(response); - const headers = this.filterHeaders(options.headersBlacklist, response.getHeaders()); - Object.assign(headers, cachedResponse.headers, { - 'cache-control': `max-age=${Math.max( - 0, - cachedResponse.duration - (new Date().getTime() - cachedResponse.date) / 1000 - ).toFixed(0)}`, - }); - response.set(headers); - // file deepcode ignore XSS: - response.status(cachedResponse.status).send(cachedResponse.body); - } - /** - * Establish the cache response and store it in the data base - * @param cacheKey - key that should be used to store the response - * @param options - Cache options - * @param request - HTTP request express object - * @param response - HTTP response express object - * @param next - Next express middleware function - */ - private setCacheResponse( - cacheKey: string, - options: CacheConfig, - request: Request, - response: Response, - next: NextFunction - ) { - const wrappedSend = response.send.bind(response); - response.send = (body: any): Response => { - if (this.isIncluded(options, request, response)) { - // Stryker disable next-line all - logger.debug(`The response will be cached`, request.uuid, this.context); - this.removeCacheHeaders(response); - response.setHeader('cache-control', `max-age=${options.duration}`); - const wrappedResponse = wrappedSend(body); - const cacheEntry = { - status: response.statusCode, - headers: this.filterHeaders(options.headersBlacklist, response.getHeaders()), - body, - date: new Date().getTime(), - duration: options.duration, - }; - this.repository.setPath(cacheKey, cacheEntry, request.uuid).catch(error => { - // Stryker disable all - logger.error( - `Error storing in cache path ${options.prefixKey + request.originalUrl}: ${ - error.message - }` - ); - // Stryker enable all - }); - return wrappedResponse; - } else { - // Stryker disable next-line all - logger.debug(`The response will NOT be cached`, request.uuid, this.context); - return wrappedSend(body); - } - }; - next(); - } - /** - * Filter the response header based in the black list option - * @param blackList - list of header that must not be stored - * @param headers - header of the response object - */ - private filterHeaders(blackList: string[], headers: OutgoingHttpHeaders): OutgoingHttpHeaders { - return Object.fromEntries( - Object.entries(headers).filter(header => !blackList.includes(header[0])) - ); - } - /** Remove cache headers */ - private removeCacheHeaders(res: Response): void { - res.removeHeader('Surrogate-Control'); - res.removeHeader('Cache-Control'); - res.removeHeader('Pragma'); - res.removeHeader('Expires'); - } - /** - * Define if a response should be cached - * @param options - Cache options to test against - * @param req - Express request object - * @param res - Express response object - */ - private isIncluded(options: CacheConfig, req: Request, res: Response): boolean { - const cachingEnabled = this.options.enabled && options.enabled; - const toggled = options.toggle && options.toggle(req, res); - - const codes = options.statusCodes; - const excluded = codes.exclude.length > 0 && codes.exclude.includes(res.statusCode); - const included = codes.include.length > 0 && codes.include.includes(res.statusCode); - return cachingEnabled && toggled && !excluded && included; - } - /** - * Create the redis key path - * @param request - HTTP request express object - * @param prefixKey - redis prefix key - * @param useBody - flag to indicate that the body must be used as part of the key - * @returns - */ - private cachePath(request: Request, prefixKey: string, useBody: boolean): string { - let baseCacheKey = `${prefixKey}${request.originalUrl}`; - if (useBody) { - const hasher = cryto.createHash('sha1'); - const bodyString = JSON.stringify(request.body || {}); - const updatedPath = hasher.update(bodyString).digest('hex'); - baseCacheKey += `:${updatedPath}`; - } - return baseCacheKey; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import logger from '@mdf.js/logger'; +import { Redis } from '@mdf.js/redis-provider'; +import cryto from 'crypto'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { OutgoingHttpHeaders } from 'http'; +import { cloneDeep, merge } from 'lodash'; +import { CacheConfig } from './CacheConfig.i'; +import { CacheEntry } from './CacheEntry.t'; +import { CacheRepository } from './CacheRepository'; + +const DEFAULT_CONFIG_CACHE_DURATION = 60; +const DEFAULT_CONFIG_CACHE_ENABLED = true; +const DEFAULT_CONFIG_CACHE_HEADERS_BLACK_LIST: string[] = []; +const DEFAULT_CONFIG_STATUS_CODES_EXCLUDED: number[] = []; +const DEFAULT_CONFIG_STATUS_CODES_INCLUDED: number[] = [200]; +const DEFAULT_CONFIG_CACHE_PREFIX = 'api:cache:'; + +const DEFAULT_CONFIG_CACHE: CacheConfig = { + duration: DEFAULT_CONFIG_CACHE_DURATION, + enabled: DEFAULT_CONFIG_CACHE_ENABLED, + headersBlacklist: DEFAULT_CONFIG_CACHE_HEADERS_BLACK_LIST, + statusCodes: { + exclude: DEFAULT_CONFIG_STATUS_CODES_EXCLUDED, + include: DEFAULT_CONFIG_STATUS_CODES_INCLUDED, + }, + useBody: false, + prefixKey: DEFAULT_CONFIG_CACHE_PREFIX, + toggle: () => true, +}; + +/** CacheRequest middleware */ +export class Cache { + /** Middleware class name */ + private readonly context: string = this.constructor.name; + /** Cache repository */ + private readonly repository: CacheRepository; + /** Cache options */ + private readonly options: CacheConfig; + /** + * Cache middleware instance + * @param client - redis client + * @returns + */ + public static instance(client: Redis.Client, options?: Partial): Cache { + return new Cache(new CacheRepository(client), options); + } + /** + * Request cache middleware handler + * @param provider - redis client + * @param options - Cache options + * @returns + */ + public static handler(provider: Redis.Client, options?: Partial): RequestHandler { + return new Cache(new CacheRepository(provider), options).handler(options); + } + /** + * Create an instance of cache middleware + * @param options - cache configuration options + * @param repository - cache repository + */ + private constructor(repository: CacheRepository, options?: Partial) { + this.repository = repository; + this.options = merge(cloneDeep(DEFAULT_CONFIG_CACHE), options); + } + /** + * Cache middleware function + * @param options - audit function + * @returns + */ + public handler(options?: Partial): RequestHandler { + const localOptions = merge(cloneDeep(this.options), options); + return (req: Request, res: Response, next: NextFunction) => { + // Stryker disable next-line all + logger.debug(`New request for path [${req.url}]`, req.uuid, this.context); + if (req.headers['x-cache-bypass']) { + next(); + } else { + const cacheKey = this.cachePath(req, localOptions.prefixKey, localOptions.useBody); + this.repository + .getPath(cacheKey, req.uuid) + .then((result: CacheEntry | null) => { + if (result !== null) { + this.useCacheResponse(localOptions, result, req, res); + } else { + this.setCacheResponse(cacheKey, localOptions, req, res, next); + } + }) + .catch((error: Error | Crash) => { + // Stryker disable next-line all + logger.error(`Error storing in cache path ${req.url}: ${error.message}`); + next(); + }); + } + }; + } + /** + * Use the cached response to fullfil the request + * @param options - Cache options + * @param request - HTTP request express object + * @param response - HTTP response express object + */ + private useCacheResponse( + options: CacheConfig, + cachedResponse: CacheEntry, + request: Request, + response: Response + ): void { + // Stryker disable next-line all + logger.debug(`The response was cached`, request.uuid, this.context); + // Stryker disable next-line all + logger.silly(`${JSON.stringify(cachedResponse, null, 2)}`, request.uuid, this.context); + this.removeCacheHeaders(response); + const headers = this.filterHeaders(options.headersBlacklist, response.getHeaders()); + Object.assign(headers, cachedResponse.headers, { + 'cache-control': `max-age=${Math.max( + 0, + cachedResponse.duration - (new Date().getTime() - cachedResponse.date) / 1000 + ).toFixed(0)}`, + }); + response.set(headers); + // file deepcode ignore XSS: + response.status(cachedResponse.status).send(cachedResponse.body); + } + /** + * Establish the cache response and store it in the data base + * @param cacheKey - key that should be used to store the response + * @param options - Cache options + * @param request - HTTP request express object + * @param response - HTTP response express object + * @param next - Next express middleware function + */ + private setCacheResponse( + cacheKey: string, + options: CacheConfig, + request: Request, + response: Response, + next: NextFunction + ) { + const wrappedSend = response.send.bind(response); + response.send = (body: any): Response => { + if (this.isIncluded(options, request, response)) { + // Stryker disable next-line all + logger.debug(`The response will be cached`, request.uuid, this.context); + this.removeCacheHeaders(response); + response.setHeader('cache-control', `max-age=${options.duration}`); + const wrappedResponse = wrappedSend(body); + const cacheEntry = { + status: response.statusCode, + headers: this.filterHeaders(options.headersBlacklist, response.getHeaders()), + body, + date: new Date().getTime(), + duration: options.duration, + }; + this.repository.setPath(cacheKey, cacheEntry, request.uuid).catch(error => { + // Stryker disable all + logger.error( + `Error storing in cache path ${options.prefixKey + request.originalUrl}: ${ + error.message + }` + ); + // Stryker enable all + }); + return wrappedResponse; + } else { + // Stryker disable next-line all + logger.debug(`The response will NOT be cached`, request.uuid, this.context); + return wrappedSend(body); + } + }; + next(); + } + /** + * Filter the response header based in the black list option + * @param blackList - list of header that must not be stored + * @param headers - header of the response object + */ + private filterHeaders(blackList: string[], headers: OutgoingHttpHeaders): OutgoingHttpHeaders { + return Object.fromEntries( + Object.entries(headers).filter(header => !blackList.includes(header[0])) + ); + } + /** Remove cache headers */ + private removeCacheHeaders(res: Response): void { + res.removeHeader('Surrogate-Control'); + res.removeHeader('Cache-Control'); + res.removeHeader('Pragma'); + res.removeHeader('Expires'); + } + /** + * Define if a response should be cached + * @param options - Cache options to test against + * @param req - Express request object + * @param res - Express response object + */ + private isIncluded(options: CacheConfig, req: Request, res: Response): boolean { + const cachingEnabled = this.options.enabled && options.enabled; + const toggled = options.toggle && options.toggle(req, res); + + const codes = options.statusCodes; + const excluded = codes.exclude.length > 0 && codes.exclude.includes(res.statusCode); + const included = codes.include.length > 0 && codes.include.includes(res.statusCode); + return cachingEnabled && toggled && !excluded && included; + } + /** + * Create the redis key path + * @param request - HTTP request express object + * @param prefixKey - redis prefix key + * @param useBody - flag to indicate that the body must be used as part of the key + * @returns + */ + private cachePath(request: Request, prefixKey: string, useBody: boolean): string { + let baseCacheKey = `${prefixKey}${request.originalUrl}`; + if (useBody) { + const hasher = cryto.createHash('sha1'); + const bodyString = JSON.stringify(request.body || {}); + const updatedPath = hasher.update(bodyString).digest('hex'); + baseCacheKey += `:${updatedPath}`; + } + return baseCacheKey; + } +} diff --git a/packages/api/middlewares/src/cache/CacheRepository.ts b/packages/api/middlewares/src/cache/CacheRepository.ts index c9d74201..7064c297 100644 --- a/packages/api/middlewares/src/cache/CacheRepository.ts +++ b/packages/api/middlewares/src/cache/CacheRepository.ts @@ -1,88 +1,89 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Crash } from '@mdf.js/crash'; -import logger from '@mdf.js/logger'; -import { Redis } from '@mdf.js/redis-provider'; -import { CacheEntry } from './CacheEntry.t'; - -const RETRIEVING_ERROR = 'Error retrieving the information from the cache'; -const DEFAULT_CONFIG_CACHE_EXPIRATION = 60; - -/** CacheRepository, cache repository management interface */ -export class CacheRepository { - /** Repository class name */ - private readonly context: string = this.constructor.name; - /** Cache client */ - private readonly redis: Redis.Client; - /** - * Create an instance of CacheRepository - * @param client - Redis client instance - */ - constructor(client: Redis.Client) { - this.redis = client; - } - /** - * Return the value of the previous response for the requested path if is present in the cache - * @param path - Route path cached - * @param uuid - Request identification, for trace propuse - */ - getPath(path: string, uuid: string): Promise { - // Stryker disable next-line all - logger.debug(`New request for path ${path}`, uuid, this.context); - if (this.redis.status !== 'ready') { - return Promise.resolve(null); - } - return new Promise((resolve, reject) => { - this.redis - .get(path) - .then((result: string | null) => { - if (typeof result !== 'string') { - resolve(null); - } else { - resolve(JSON.parse(result)); - } - }) - .catch((error: Error) => { - reject(new Crash(RETRIEVING_ERROR, uuid, { cause: error })); - }); - }); - } - /** - * Return the value of the previous response for the requested path if is present in the cache - * @param path - Route path cached - * @param response - Response to store in the cache, must be a string - * @param uuid - Request identification, for trace propuse - */ - setPath(path: string, response: CacheEntry, uuid: string): Promise { - // Stryker disable next-line all - logger.debug(`New request for path ${path}`, uuid, this.context); - // Stryker disable next-line all - logger.silly(`${response}`, uuid, this.context); - if (this.redis.status !== 'ready') { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - this.redis - .setex(path, response.duration || DEFAULT_CONFIG_CACHE_EXPIRATION, JSON.stringify(response)) - .then((result: 'OK') => { - // Stryker disable all - logger.debug( - `Path ${path} stored in cache with delay of ${ - response.duration || DEFAULT_CONFIG_CACHE_EXPIRATION - } seconds`, - uuid, - this.context - ); - // Stryker enable all - resolve(); - }) - .catch((error: Error) => { - reject(new Crash(RETRIEVING_ERROR, uuid, { cause: error })); - }); - }); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Crash } from '@mdf.js/crash'; +import logger from '@mdf.js/logger'; +import { Redis } from '@mdf.js/redis-provider'; +import { CacheEntry } from './CacheEntry.t'; + +const RETRIEVING_ERROR = 'Error retrieving the information from the cache'; +const DEFAULT_CONFIG_CACHE_EXPIRATION = 60; + +/** CacheRepository, cache repository management interface */ +export class CacheRepository { + /** Repository class name */ + private readonly context: string = this.constructor.name; + /** Cache client */ + private readonly redis: Redis.Client; + /** + * Create an instance of CacheRepository + * @param client - Redis client instance + */ + constructor(client: Redis.Client) { + this.redis = client; + } + /** + * Return the value of the previous response for the requested path if is present in the cache + * @param path - Route path cached + * @param uuid - Request identification, for trace propuse + */ + getPath(path: string, uuid: string): Promise { + // Stryker disable next-line all + logger.debug(`New request for path ${path}`, uuid, this.context); + if (this.redis.status !== 'ready') { + return Promise.resolve(null); + } + return new Promise((resolve, reject) => { + this.redis + .get(path) + .then((result: string | null) => { + if (typeof result !== 'string') { + resolve(null); + } else { + resolve(JSON.parse(result)); + } + }) + .catch((error: Error) => { + reject(new Crash(RETRIEVING_ERROR, uuid, { cause: error })); + }); + }); + } + /** + * Return the value of the previous response for the requested path if is present in the cache + * @param path - Route path cached + * @param response - Response to store in the cache, must be a string + * @param uuid - Request identification, for trace propuse + */ + setPath(path: string, response: CacheEntry, uuid: string): Promise { + // Stryker disable next-line all + logger.debug(`New request for path ${path}`, uuid, this.context); + // Stryker disable next-line all + logger.silly(`${JSON.stringify(response)}`, uuid, this.context); + if (this.redis.status !== 'ready') { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.redis + .setex(path, response.duration || DEFAULT_CONFIG_CACHE_EXPIRATION, JSON.stringify(response)) + .then((result: 'OK') => { + // Stryker disable all + logger.debug( + `Path ${path} stored in cache with delay of ${ + response.duration || DEFAULT_CONFIG_CACHE_EXPIRATION + } seconds`, + uuid, + this.context + ); + // Stryker enable all + resolve(); + }) + .catch((error: Error) => { + reject(new Crash(RETRIEVING_ERROR, uuid, { cause: error })); + }); + }); + } +} + diff --git a/packages/api/middlewares/src/cors/cors.ts b/packages/api/middlewares/src/cors/cors.ts index 8d776d7f..6ed991e0 100644 --- a/packages/api/middlewares/src/cors/cors.ts +++ b/packages/api/middlewares/src/cors/cors.ts @@ -1,102 +1,102 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import CorsModule from 'cors'; -import { RequestHandler } from 'express'; -import { CorsConfig } from './CorsConfig.i'; - -/** CorsManagement class manages the API request CORS */ -export class Cors { - /** Flag that indicates that CORS is enabled */ - private readonly enable: boolean; - /** CORS configuration */ - private readonly config?: CorsConfig; - /** CORS Options */ - private readonly corsOptions: CorsModule.CorsOptions = {}; - /** Cors middleware */ - private readonly corsModule: RequestHandler; - /** Origin whitelist */ - private readonly whitelist: (string | RegExp)[] = []; - /** Return a cors handler middleware instance */ - public static handler(configuration?: CorsConfig): RequestHandler { - return new Cors(configuration).handler; - } - /** Return a cors handler middleware instance */ - private get handler(): RequestHandler { - return this.corsModule; - } - /** - * Create an instance of CorsManagement - * @param configuration - CORS Configuration - */ - private constructor(configuration?: CorsConfig) { - this.config = configuration; - this.enable = this.config && this.config.enabled === false ? false : true; - // ***************************************************************************************** - // #region Cors options - this.corsOptions.methods = this.config?.methods; - this.corsOptions.allowedHeaders = this.config?.allowHeaders; - this.corsOptions.exposedHeaders = this.config?.exposedHeaders; - this.corsOptions.credentials = this.config?.credentials; - this.corsOptions.maxAge = this.config?.maxAge; - this.corsOptions.preflightContinue = this.config?.preflightContinue; - this.corsOptions.optionsSuccessStatus = this.config?.optionsSuccessStatus; - - this.whitelist = this.adaptWhiteList(this.config?.whitelist); - if (!this.enable) { - this.corsOptions.origin = false; - } else if (!this.config?.whitelist) { - this.corsOptions.origin = true; - } else if (!this.config?.allowAppClients) { - this.corsOptions.origin = this.whitelist; - } else { - this.corsOptions.origin = this.originFilter; - } - this.corsModule = CorsModule(this.corsOptions); - } - /** - * Adapt the whitelist parameter to the CORS module - * @param whitelist - Whitelist of allowed dominio - */ - private adaptWhiteList(whitelist?: string | (string | RegExp)[]): (string | RegExp)[] { - if (!whitelist) { - return [new RegExp(/.*/)]; - } else if (typeof whitelist === 'string') { - if (whitelist === '*') { - return [new RegExp(/.*/)]; - } else { - return [whitelist]; - } - } else { - return whitelist; - } - } - /** - * Handle a request to define if it allowed by the CORS configuration - * @param requestOrigin - request to analyze - * @param callback - callback to define if the request is allowed or not - */ - private readonly originFilter = ( - requestOrigin: string | undefined, - callback: (err: Error | null, allow: boolean) => void - ): void => { - let regexMatch = false; - if (!requestOrigin || this.whitelist.indexOf(requestOrigin) !== -1) { - callback(null, true); - } else { - this.whitelist.forEach(allowedOrigin => { - if (allowedOrigin instanceof RegExp && !regexMatch) { - regexMatch = allowedOrigin.test(requestOrigin); - } - }); - if (regexMatch) { - callback(null, true); - } else { - callback(null, false); - } - } - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import CorsModule from 'cors'; +import { RequestHandler } from 'express'; +import { CorsConfig } from './CorsConfig.i'; + +/** CorsManagement class manages the API request CORS */ +export class Cors { + /** Flag that indicates that CORS is enabled */ + private readonly enable: boolean; + /** CORS configuration */ + private readonly config?: CorsConfig; + /** CORS Options */ + private readonly corsOptions: CorsModule.CorsOptions = {}; + /** Cors middleware */ + private readonly corsModule: RequestHandler; + /** Origin whitelist */ + private readonly whitelist: (string | RegExp)[] = []; + /** Return a cors handler middleware instance */ + public static handler(configuration?: CorsConfig): RequestHandler { + return new Cors(configuration).handler; + } + /** Return a cors handler middleware instance */ + private get handler(): RequestHandler { + return this.corsModule; + } + /** + * Create an instance of CorsManagement + * @param configuration - CORS Configuration + */ + private constructor(configuration?: CorsConfig) { + this.config = configuration; + this.enable = this.config?.enabled !== false; + // ***************************************************************************************** + // #region Cors options + this.corsOptions.methods = this.config?.methods; + this.corsOptions.allowedHeaders = this.config?.allowHeaders; + this.corsOptions.exposedHeaders = this.config?.exposedHeaders; + this.corsOptions.credentials = this.config?.credentials; + this.corsOptions.maxAge = this.config?.maxAge; + this.corsOptions.preflightContinue = this.config?.preflightContinue; + this.corsOptions.optionsSuccessStatus = this.config?.optionsSuccessStatus; + + this.whitelist = this.adaptWhiteList(this.config?.whitelist); + if (!this.enable) { + this.corsOptions.origin = false; + } else if (!this.config?.whitelist) { + this.corsOptions.origin = true; + } else if (!this.config?.allowAppClients) { + this.corsOptions.origin = this.whitelist; + } else { + this.corsOptions.origin = this.originFilter; + } + this.corsModule = CorsModule(this.corsOptions); + } + /** + * Adapt the whitelist parameter to the CORS module + * @param whitelist - Whitelist of allowed dominio + */ + private adaptWhiteList(whitelist?: string | (string | RegExp)[]): (string | RegExp)[] { + if (!whitelist) { + return [new RegExp(/.*/)]; + } else if (typeof whitelist === 'string') { + if (whitelist === '*') { + return [new RegExp(/.*/)]; + } else { + return [whitelist]; + } + } else { + return whitelist; + } + } + /** + * Handle a request to define if it allowed by the CORS configuration + * @param requestOrigin - request to analyze + * @param callback - callback to define if the request is allowed or not + */ + private readonly originFilter = ( + requestOrigin: string | undefined, + callback: (err: Error | null, allow: boolean) => void + ): void => { + let regexMatch = false; + if (!requestOrigin || this.whitelist.indexOf(requestOrigin) !== -1) { + callback(null, true); + } else { + this.whitelist.forEach(allowedOrigin => { + if (allowedOrigin instanceof RegExp && !regexMatch) { + regexMatch = allowedOrigin.test(requestOrigin); + } + }); + if (regexMatch) { + callback(null, true); + } else { + callback(null, false); + } + } + }; +} diff --git a/packages/api/middlewares/src/index.ts b/packages/api/middlewares/src/index.ts index 8e04da8e..7be272a6 100644 --- a/packages/api/middlewares/src/index.ts +++ b/packages/api/middlewares/src/index.ts @@ -1,64 +1,60 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Audit } from './audit'; -import { AuthZ } from './authz'; -import { BodyParser } from './bodyParser'; -import { Cache } from './cache'; -import { Cors } from './cors'; -import { Default } from './default'; -import { ErrorHandler } from './errorHandler'; -import { Security } from './helmet'; -import { Logger } from './logger'; -import { Metrics } from './metrics'; -import { Multer } from './multer'; -import { NoCache } from './nocache'; -import { RateLimiter } from './rateLimiter'; -import { RequestId } from './requestId'; - -export type { Audit } from './audit'; -export type { AuthZ } from './authz'; -export type { BodyParser } from './bodyParser'; -export type { Cache } from './cache'; -export type { Cors } from './cors'; -export type { Default } from './default'; -export type { ErrorHandler } from './errorHandler'; -export type { Security } from './helmet'; -export type { Logger } from './logger'; -export type { Metrics } from './metrics'; -export type { Multer } from './multer'; -export type { NoCache } from './nocache'; -export type { RateLimiter } from './rateLimiter'; -export type { RequestId } from './requestId'; - -export const Middleware = { - Audit, - AuthZ, - BodyParser, - Cache, - Cors, - Default, - ErrorHandler, - Security, - Logger, - Metrics, - Multer, - NoCache, - RateLimiter, - RequestId, -}; - -export type { AuditCategory, AuditConfig } from './audit'; -export type { CacheConfig } from './cache'; -export type { CorsConfig } from './cors'; -export type { RateLimitConfig } from './rateLimiter'; -export type { - AfterRoutesMiddlewares, - BeforeRoutesMiddlewares, - EndpointsMiddlewares, - Middlewares, -} from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Audit } from './audit'; +import { AuthZ } from './authz'; +import { BodyParser } from './bodyParser'; +import { Cache } from './cache'; +import { Cors } from './cors'; +import { Default } from './default'; +import { ErrorHandler } from './errorHandler'; +import { Security } from './helmet'; +import { Logger } from './logger'; +import { Metrics } from './metrics'; +import { Multer } from './multer'; +import { NoCache } from './nocache'; +import { RateLimiter } from './rateLimiter'; +import { RequestId } from './requestId'; + +export type { + AfterRoutesMiddlewares, + BeforeRoutesMiddlewares, + EndpointsMiddlewares, + Middlewares, +} from './types'; + +export type { Audit, AuditCategory, AuditConfig } from './audit'; +export type { AuthZ, AuthZOptions } from './authz'; +export type { BodyParser } from './bodyParser'; +export type { Cache, CacheConfig } from './cache'; +export type { Cors, CorsConfig } from './cors'; +export type { Default } from './default'; +export type { ErrorHandler } from './errorHandler'; +export type { Security } from './helmet'; +export type { Logger } from './logger'; +export type { Metrics } from './metrics'; +export type { Multer } from './multer'; +export type { NoCache } from './nocache'; +export type { RateLimitConfig, RateLimitEntry, RateLimiter } from './rateLimiter'; +export type { RequestId } from './requestId'; + +export const Middleware = { + Audit, + AuthZ, + BodyParser, + Cache, + Cors, + Default, + ErrorHandler, + Security, + Logger, + Metrics, + Multer, + NoCache, + RateLimiter, + RequestId, +}; diff --git a/packages/api/middlewares/src/logger/logger.ts b/packages/api/middlewares/src/logger/logger.ts index 28a894bc..747d7b54 100644 --- a/packages/api/middlewares/src/logger/logger.ts +++ b/packages/api/middlewares/src/logger/logger.ts @@ -1,71 +1,72 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { LoggerInstance } from '@mdf.js/logger'; -import { RequestHandler } from 'express'; -import { IncomingMessage, ServerResponse } from 'http'; -import morgan from 'morgan'; - -type Handler = ( - req: Request, - res: Response, - callback: (err?: Error) => void -) => void; - -/** - * Auxiliar function that map log levels to HTTP status codes - * @param status - HTTP status code - * @returns Log level for the HTTP status code - */ -function logLevelPerStatus(status: number): string { - if (status >= 500) { - return 'error'; - } else if (status >= 400 && status < 500) { - return 'warn'; - } else { - return 'debug'; - } -} -/** - * Express middleware logger handler - * @param color - colored output, note: only in NODE_ENV: development mode - * @param stream - @netin-js/logger stream - * @returns Express middleware handler - */ -function expressLogger< - Request extends IncomingMessage = IncomingMessage, - Response extends ServerResponse = ServerResponse, ->(logger: LoggerInstance): Handler { - return morgan( - (tokens: morgan.TokenIndexer, req: Request, res: Response) => { - const status = tokens['status'](req, res) || '-'; - const timestamp = tokens['date'](req, res, 'iso') || '-'; - let str = `HTTP/${tokens['http-version'](req, res)} `; - str += `${tokens['method'](req, res)} `; - str += `${status} `; - str += `${tokens['url'](req, res)} - `; - str += `${tokens['total-time'](req, res)} ms - `; - str += `${tokens['res'](req, res, 'content-length')} bytes - `; - str += `${tokens['remote-addr'](req, res)}`; - return JSON.stringify({ - uuid: req.headers['X-Request-ID'], - level: logLevelPerStatus(parseInt(status)), - status, - timestamp, - context: 'express', - message: str, - }); - }, - { stream: logger.stream } - ); -} - -export class LoggerMiddleware { - /** Request logger middleware handler */ - public static handler(logger: LoggerInstance): RequestHandler { - return expressLogger(logger); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { LoggerInstance } from '@mdf.js/logger'; +import { RequestHandler } from 'express'; +import { IncomingMessage, ServerResponse } from 'http'; +import morgan from 'morgan'; + +type Handler = ( + req: Request, + res: Response, + callback: (err?: Error) => void +) => void; + +/** + * Auxiliar function that map log levels to HTTP status codes + * @param status - HTTP status code + * @returns Log level for the HTTP status code + */ +function logLevelPerStatus(status: number): string { + if (status >= 500) { + return 'error'; + } else if (status >= 400 && status < 500) { + return 'warn'; + } else { + return 'debug'; + } +} +/** + * Express middleware logger handler + * @param color - colored output, note: only in NODE_ENV: development mode + * @param stream - @netin-js/logger stream + * @returns Express middleware handler + */ +function expressLogger< + Request extends IncomingMessage = IncomingMessage, + Response extends ServerResponse = ServerResponse, +>(logger: LoggerInstance): Handler { + return morgan( + (tokens: morgan.TokenIndexer, req: Request, res: Response) => { + const status = tokens['status'](req, res) ?? '-'; + const timestamp = tokens['date'](req, res, 'iso') ?? '-'; + let str = `HTTP/${tokens['http-version'](req, res)} `; + str += `${tokens['method'](req, res)} `; + str += `${status} `; + str += `${tokens['url'](req, res)} - `; + str += `${tokens['total-time'](req, res)} ms - `; + str += `${tokens['res'](req, res, 'content-length')} bytes - `; + str += `${tokens['remote-addr'](req, res)}`; + return JSON.stringify({ + uuid: req.headers['X-Request-ID'], + level: logLevelPerStatus(parseInt(status)), + status, + timestamp, + context: 'express', + message: str, + }); + }, + { stream: logger.stream } + ); +} + +export class LoggerMiddleware { + /** Request logger middleware handler */ + public static handler(logger: LoggerInstance): RequestHandler { + return expressLogger(logger); + } +} + diff --git a/packages/api/middlewares/src/metrics/metrics.ts b/packages/api/middlewares/src/metrics/metrics.ts index e617a55c..7dde5cc8 100644 --- a/packages/api/middlewares/src/metrics/metrics.ts +++ b/packages/api/middlewares/src/metrics/metrics.ts @@ -1,250 +1,250 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { NextFunction, Request, RequestHandler, Response } from 'express'; -import { Counter, Gauge, Histogram, Registry, register } from 'prom-client'; - -/** Global metric for API */ -const METRICS_DEFINITIONS = (prefix: string = '', registry: Registry): MetricInstances => { - return { - api_all_request_total: - (registry.getSingleMetric(`${prefix}api_all_request_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_request_total`, - help: 'The total number of all API requests received', - registers: [registry], - }), - api_all_info_total: - (registry.getSingleMetric(`${prefix}api_all_info_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_info_total`, - help: 'The total number of all API requests with informative response', - registers: [registry], - }), - api_all_success_total: - (registry.getSingleMetric(`${prefix}api_all_success_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_success_total`, - help: 'The total number of all API requests with success response', - registers: [registry], - }), - api_all_redirect_total: - (registry.getSingleMetric(`${prefix}api_all_redirect_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_redirect_total`, - help: 'The total number of all API requests with redirect response', - registers: [registry], - }), - api_all_errors_total: - (registry.getSingleMetric(`${prefix}api_all_errors_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_errors_total`, - help: 'The total number of all API requests with error response', - registers: [registry], - }), - api_all_client_error_total: - (registry.getSingleMetric(`${prefix}api_all_client_error_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_client_error_total`, - help: 'The total number of all API requests with client error response', - registers: [registry], - }), - api_all_server_error_total: - (registry.getSingleMetric(`${prefix}api_all_server_error_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_all_server_error_total`, - help: 'The total number of all API requests with server error response', - registers: [registry], - }), - api_all_request_in_processing_total: - (registry.getSingleMetric(`${prefix}api_all_request_in_processing_total`) as - | Gauge - | undefined) ?? - new Gauge({ - name: `${prefix}api_all_request_in_processing_total`, - help: 'The total number of all API requests currently in processing (no response yet)', - registers: [registry], - }), - /** API Operation counters, labeled with method, path and code */ - api_request_total: - (registry.getSingleMetric(`${prefix}api_request_total`) as Counter | undefined) ?? - new Counter({ - name: `${prefix}api_request_total`, - help: 'The total number of all API requests', - labelNames: ['method', 'path', 'code'], - registers: [registry], - }), - /** API request duration histogram, labeled with method, path and code */ - api_request_duration_milliseconds: - (registry.getSingleMetric(`${prefix}api_request_duration_milliseconds`) as - | Histogram - | undefined) ?? - new Histogram({ - name: `${prefix}api_request_duration_milliseconds`, - help: 'API requests duration', - labelNames: ['method', 'path', 'code'], - registers: [registry], - buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], - }), - api_request_size_bytes: - (registry.getSingleMetric(`${prefix}api_request_size_bytes`) as Histogram | undefined) ?? - new Histogram({ - name: `${prefix}api_request_size_bytes`, - help: 'API requests size', - labelNames: ['method', 'path', 'code'], - registers: [registry], - buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], - }), - api_response_size_bytes: - (registry.getSingleMetric(`${prefix}api_response_size_bytes`) as Histogram | undefined) ?? - new Histogram({ - name: `${prefix}api_response_size_bytes`, - help: 'API requests size', - labelNames: ['method', 'path', 'code'], - registers: [registry], - buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], - }), - }; -}; -/** Metric types */ -type MetricInstances = { - /** The total number of all API requests received */ - api_all_request_total: Counter; - /** The total number of all API requests with informative response */ - api_all_info_total: Counter; - /** The total number of all API requests with success response */ - api_all_success_total: Counter; - /** The total number of all API requests with redirect response */ - api_all_redirect_total: Counter; - /** The total number of all API requests with error response */ - api_all_errors_total: Counter; - /** The total number of all API requests with client error response */ - api_all_client_error_total: Counter; - /** The total number of all API requests with server error response */ - api_all_server_error_total: Counter; - /** The total number of all API requests currently in processing (no response yet) */ - api_all_request_in_processing_total: Gauge; - /** The total number of all API requests */ - api_request_total: Counter; - /** API requests duration */ - api_request_duration_milliseconds: Histogram; - /** API requests size */ - api_request_size_bytes: Histogram; - /** API requests size */ - api_response_size_bytes: Histogram; -}; -const CONTENT_LENGTH_HEADER = 'content-length'; -/** MetricsExpressMiddleware middleware */ -export class MetricsMiddleware { - /** Core and API metrics */ - private readonly metrics: MetricInstances; - /** - * Return a metrics middleware instance - * @param registry - Metrics registry interface - * @param prefix - Metrics prefix - */ - public static handler(prefix?: string): RequestHandler; - /** - * Return a metrics middleware instance - * @param registry - Metrics registry interface - * @param prefix - Metrics prefix - */ - public static handler(registry?: Registry, prefix?: string): RequestHandler; - public static handler(registry?: Registry | string, prefix?: string): RequestHandler { - if (typeof registry === 'string') { - return new MetricsMiddleware(undefined, registry).handler; - } else { - return new MetricsMiddleware(registry, prefix).handler; - } - } - /** - * Create a new instance of metrics express middleware class - * @param registry - Metrics registry interface - * @param prefix - Metrics prefix - */ - private constructor( - private readonly registry: Registry = register, - prefix: string = '' - ) { - this.metrics = METRICS_DEFINITIONS(prefix, this.registry); - } - /** Return response status code class */ - private increaseCounterMetricByCode(code: number): void { - if (!this.registry || !this.metrics) { - return; - } else if (code < 200) { - this.metrics['api_all_info_total'].inc(); - } else if (code < 300) { - this.metrics['api_all_success_total'].inc(); - } else if (code < 400) { - this.metrics['api_all_redirect_total'].inc(); - } else if (code < 500) { - this.metrics['api_all_errors_total'].inc(); - this.metrics['api_all_client_error_total'].inc(); - } else { - this.metrics['api_all_errors_total'].inc(); - this.metrics['api_all_server_error_total'].inc(); - } - } - /** - * Return the content length for request and response objects - * @param request - HTTP request express object - * @param response - HTTP response express object - */ - private getContentLength( - request: Request, - response: Response - ): { request: number; response: number } { - const reqHeaderContentLength = request.headers[CONTENT_LENGTH_HEADER] - ? parseInt(request.headers[CONTENT_LENGTH_HEADER]) - : 0; - const resHeaderContentLength = response.getHeader(CONTENT_LENGTH_HEADER); - let responseLength: number; - if (typeof resHeaderContentLength === 'string') { - const parsedLength = parseInt(resHeaderContentLength); - responseLength = Number.isNaN(parsedLength) ? 0 : parsedLength; - } else if (typeof resHeaderContentLength === 'number') { - responseLength = resHeaderContentLength; - } else { - responseLength = 0; - } - return { request: reqHeaderContentLength, response: responseLength }; - } - /** - * Express handler function - * @param request - HTTP request express object - * @param response - HTTP response express object - * @param next - Next express middleware function - */ - private get handler(): RequestHandler { - return (request: Request, response: Response, next: NextFunction): void => { - this.metrics['api_all_request_total'].inc(); - this.metrics['api_all_request_in_processing_total'].inc(); - const startDate = new Date().getTime(); - response.on('finish', () => { - if (!this.registry || !this.metrics) { - return; - } - const labels = { - method: request.method, - path: request.route ? request.route.path : request.originalUrl, - code: response.statusCode, - }; - const endDate = new Date().getTime(); - const duration = endDate - startDate; - const contentLength = this.getContentLength(request, response); - this.metrics['api_all_request_in_processing_total'].dec(); - this.increaseCounterMetricByCode(response.statusCode); - this.metrics['api_request_total'].inc(labels); - this.metrics['api_request_duration_milliseconds'].observe(labels, duration); - this.metrics['api_request_size_bytes'].observe(labels, contentLength.request); - this.metrics['api_response_size_bytes'].observe(labels, contentLength.response); - }); - next(); - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { Counter, Gauge, Histogram, Registry, register } from 'prom-client'; + +/** Global metric for API */ +const METRICS_DEFINITIONS = (prefix: string = '', registry: Registry): MetricInstances => { + return { + api_all_request_total: + (registry.getSingleMetric(`${prefix}api_all_request_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_request_total`, + help: 'The total number of all API requests received', + registers: [registry], + }), + api_all_info_total: + (registry.getSingleMetric(`${prefix}api_all_info_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_info_total`, + help: 'The total number of all API requests with informative response', + registers: [registry], + }), + api_all_success_total: + (registry.getSingleMetric(`${prefix}api_all_success_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_success_total`, + help: 'The total number of all API requests with success response', + registers: [registry], + }), + api_all_redirect_total: + (registry.getSingleMetric(`${prefix}api_all_redirect_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_redirect_total`, + help: 'The total number of all API requests with redirect response', + registers: [registry], + }), + api_all_errors_total: + (registry.getSingleMetric(`${prefix}api_all_errors_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_errors_total`, + help: 'The total number of all API requests with error response', + registers: [registry], + }), + api_all_client_error_total: + (registry.getSingleMetric(`${prefix}api_all_client_error_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_client_error_total`, + help: 'The total number of all API requests with client error response', + registers: [registry], + }), + api_all_server_error_total: + (registry.getSingleMetric(`${prefix}api_all_server_error_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_all_server_error_total`, + help: 'The total number of all API requests with server error response', + registers: [registry], + }), + api_all_request_in_processing_total: + (registry.getSingleMetric(`${prefix}api_all_request_in_processing_total`) as + | Gauge + | undefined) ?? + new Gauge({ + name: `${prefix}api_all_request_in_processing_total`, + help: 'The total number of all API requests currently in processing (no response yet)', + registers: [registry], + }), + /** API Operation counters, labeled with method, path and code */ + api_request_total: + (registry.getSingleMetric(`${prefix}api_request_total`) as Counter | undefined) ?? + new Counter({ + name: `${prefix}api_request_total`, + help: 'The total number of all API requests', + labelNames: ['method', 'path', 'code'], + registers: [registry], + }), + /** API request duration histogram, labeled with method, path and code */ + api_request_duration_milliseconds: + (registry.getSingleMetric(`${prefix}api_request_duration_milliseconds`) as + | Histogram + | undefined) ?? + new Histogram({ + name: `${prefix}api_request_duration_milliseconds`, + help: 'API requests duration', + labelNames: ['method', 'path', 'code'], + registers: [registry], + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + }), + api_request_size_bytes: + (registry.getSingleMetric(`${prefix}api_request_size_bytes`) as Histogram | undefined) ?? + new Histogram({ + name: `${prefix}api_request_size_bytes`, + help: 'API requests size', + labelNames: ['method', 'path', 'code'], + registers: [registry], + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + }), + api_response_size_bytes: + (registry.getSingleMetric(`${prefix}api_response_size_bytes`) as Histogram | undefined) ?? + new Histogram({ + name: `${prefix}api_response_size_bytes`, + help: 'API requests size', + labelNames: ['method', 'path', 'code'], + registers: [registry], + buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], + }), + }; +}; +/** Metric types */ +type MetricInstances = { + /** The total number of all API requests received */ + api_all_request_total: Counter; + /** The total number of all API requests with informative response */ + api_all_info_total: Counter; + /** The total number of all API requests with success response */ + api_all_success_total: Counter; + /** The total number of all API requests with redirect response */ + api_all_redirect_total: Counter; + /** The total number of all API requests with error response */ + api_all_errors_total: Counter; + /** The total number of all API requests with client error response */ + api_all_client_error_total: Counter; + /** The total number of all API requests with server error response */ + api_all_server_error_total: Counter; + /** The total number of all API requests currently in processing (no response yet) */ + api_all_request_in_processing_total: Gauge; + /** The total number of all API requests */ + api_request_total: Counter; + /** API requests duration */ + api_request_duration_milliseconds: Histogram; + /** API requests size */ + api_request_size_bytes: Histogram; + /** API requests size */ + api_response_size_bytes: Histogram; +}; +const CONTENT_LENGTH_HEADER = 'content-length'; +/** MetricsExpressMiddleware middleware */ +export class MetricsMiddleware { + /** Core and API metrics */ + private readonly metrics: MetricInstances; + /** + * Return a metrics middleware instance + * @param prefix - Metrics prefix + */ + public static handler(prefix?: string): RequestHandler; + /** + * Return a metrics middleware instance + * @param registry - Metrics registry interface + * @param prefix - Metrics prefix + */ + public static handler(registry?: Registry, prefix?: string): RequestHandler; + public static handler(registry?: Registry | string, prefix?: string): RequestHandler { + if (typeof registry === 'string') { + return new MetricsMiddleware(undefined, registry).handler; + } else { + return new MetricsMiddleware(registry, prefix).handler; + } + } + /** + * Create a new instance of metrics express middleware class + * @param registry - Metrics registry interface + * @param prefix - Metrics prefix + */ + private constructor( + private readonly registry: Registry = register, + prefix: string = '' + ) { + this.metrics = METRICS_DEFINITIONS(prefix, this.registry); + } + /** Return response status code class */ + private increaseCounterMetricByCode(code: number): void { + if (!this.registry || !this.metrics) { + return; + } else if (code < 200) { + this.metrics['api_all_info_total'].inc(); + } else if (code < 300) { + this.metrics['api_all_success_total'].inc(); + } else if (code < 400) { + this.metrics['api_all_redirect_total'].inc(); + } else if (code < 500) { + this.metrics['api_all_errors_total'].inc(); + this.metrics['api_all_client_error_total'].inc(); + } else { + this.metrics['api_all_errors_total'].inc(); + this.metrics['api_all_server_error_total'].inc(); + } + } + /** + * Return the content length for request and response objects + * @param request - HTTP request express object + * @param response - HTTP response express object + */ + private getContentLength( + request: Request, + response: Response + ): { request: number; response: number } { + const reqHeaderContentLength = request.headers[CONTENT_LENGTH_HEADER] + ? parseInt(request.headers[CONTENT_LENGTH_HEADER]) + : 0; + const resHeaderContentLength = response.getHeader(CONTENT_LENGTH_HEADER); + let responseLength: number; + if (typeof resHeaderContentLength === 'string') { + const parsedLength = parseInt(resHeaderContentLength); + responseLength = Number.isNaN(parsedLength) ? 0 : parsedLength; + } else if (typeof resHeaderContentLength === 'number') { + responseLength = resHeaderContentLength; + } else { + responseLength = 0; + } + return { request: reqHeaderContentLength, response: responseLength }; + } + /** + * Express handler function + * @param request - HTTP request express object + * @param response - HTTP response express object + * @param next - Next express middleware function + */ + private get handler(): RequestHandler { + return (request: Request, response: Response, next: NextFunction): void => { + this.metrics['api_all_request_total'].inc(); + this.metrics['api_all_request_in_processing_total'].inc(); + const startDate = new Date().getTime(); + response.on('finish', () => { + if (!this.registry || !this.metrics) { + return; + } + const labels = { + method: request.method, + path: request.route ? request.route.path : request.originalUrl, + code: response.statusCode, + }; + const endDate = new Date().getTime(); + const duration = endDate - startDate; + const contentLength = this.getContentLength(request, response); + this.metrics['api_all_request_in_processing_total'].dec(); + this.increaseCounterMetricByCode(response.statusCode); + this.metrics['api_request_total'].inc(labels); + this.metrics['api_request_duration_milliseconds'].observe(labels, duration); + this.metrics['api_request_size_bytes'].observe(labels, contentLength.request); + this.metrics['api_response_size_bytes'].observe(labels, contentLength.response); + }); + next(); + }; + } +} + diff --git a/packages/api/middlewares/src/multer/multer.ts b/packages/api/middlewares/src/multer/multer.ts index 048cd4a7..73f77dee 100644 --- a/packages/api/middlewares/src/multer/multer.ts +++ b/packages/api/middlewares/src/multer/multer.ts @@ -1,270 +1,271 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { BoomHelpers, Crash, Multi } from '@mdf.js/crash'; -import { NextFunction, Request, RequestHandler, Response } from 'express'; -import { merge } from 'lodash'; -import multer, { FileFilterCallback, MulterError, Options, StorageEngine } from 'multer'; - -const MEMORY_STORAGE = multer.memoryStorage(); - -export type MulterLimits = Options['limits']; - -// ************************************************************************************************* -// #region Multer limits and configuration default values -/** Max field name size (in bytes) */ -const DEFAULT_CONFIG_API_MAX_FORM_FIELD_SIZE = 100; -/** Max field value size (in bytes) */ -const DEFAULT_CONFIG_API_MAX_FIELD_SIZE = 1048576; -/** Max number of non-file fields */ -const DEFAULT_CONFIG_API_MAX_FIELDS = 100; -/** For multipart forms, the max file size (in bytes)*/ -const DEFAULT_CONFIG_API_MAX_UPLOAD_FILE_SIZE = 1048576 * 1; -/** For multipart forms, the max number of file fields */ -const DEFAULT_CONFIG_API_MAX_FILES = 10; -/** For multipart forms, the max number of parts (fields + files) */ -const DEFAULT_CONFIG_API_MAX_PARTS = 100; -/** For multipart forms, the max number of header key=>value pairs to parse */ -const DEFAULT_CONFIG_API_MAX_HEADERS = 2000; -// #endregion -/** - * Multer middleware wrapping for multipart/from-data - * @remarks WARNING: - * Make sure that you always handle the files that a user uploads. Never add multer as a global - * middleware since a malicious user could upload files to a route that you didn't anticipate. Only - * use this function on routes where you are handling the uploaded files. - */ -export class Multer { - /** Allowed mime types allowed for this multer instance */ - private readonly allowedMimeTypes: string[] = []; - /** Instance of multer */ - private readonly instance: multer.Multer; - /** - * Return a new instance of the multipart/form-data middleware - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static Instance( - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): Multer { - return new Multer(storage, allowedMineTypes, limits); - } - /** - * Return a new instance of the multipart/form-data middleware that accepts a single file with the - * name fieldName. The single file will be stored in req.file - * @param fieldName - name of the file field - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static SingleHandler( - fieldName: string, - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): RequestHandler { - return new Multer(storage, allowedMineTypes, limits).single(fieldName); - } - /** - * Return a new instance of the multipart/form-data middleware that accepts an array of files, all - * with the name fieldName. Optionally error out if more than maxCount files are uploaded. The - * array of files will be stored in req.files - * @param fieldName - name of the file field - * @param maxCount - maximum number of files - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static ArrayHandler( - fieldName: string, - maxCount?: number, - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): RequestHandler { - return new Multer(storage, allowedMineTypes, limits).array(fieldName, maxCount); - } - /** - * Return a new instance of the multipart/form-data middleware that accepts a mix of files, - * specified by fields. An object with arrays of files will be stored in req.files. - * @param fields - array of entries - * - * @example - * - * ``` - * [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }] - * ``` - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static FieldsHandler( - fields: readonly multer.Field[], - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): RequestHandler { - return new Multer(storage, allowedMineTypes, limits).fields(fields); - } - /** - * Return a new instance of the multipart/form-data middleware that accepts only text fields. If - * any file upload is made, error with code "Unexpected field" will be issued. - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static NoneHandler( - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): RequestHandler { - return new Multer(storage, allowedMineTypes, limits).none(); - } - /** - * Return a new instance of the multipart/form-data middleware that accepts all files that comes - * over the wire. An array of files will be stored in req.files. - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - static AnyHandler( - storage?: StorageEngine, - allowedMineTypes?: string | string[], - limits?: MulterLimits - ): RequestHandler { - return new Multer(storage, allowedMineTypes, limits).any(); - } - /** - * Return a new instance of the multipart/form-data middleware - * @param storage - storage engine used for this middleware - * @param allowedMineTypes - Allowed mime types allowed for this multer instance - * @param limits - Limits for the middleware - */ - private constructor( - storage: StorageEngine = MEMORY_STORAGE, - allowedMineTypes: string | string[] = [], - limits: MulterLimits - ) { - if (typeof allowedMineTypes === 'string' || Array.isArray(allowedMineTypes)) { - this.allowedMimeTypes = - typeof allowedMineTypes === 'string' ? [allowedMineTypes] : allowedMineTypes; - } - this.instance = multer({ - storage, - limits: merge(limits, { - fieldNameSize: DEFAULT_CONFIG_API_MAX_FORM_FIELD_SIZE, - fieldSize: DEFAULT_CONFIG_API_MAX_FIELD_SIZE, - fields: DEFAULT_CONFIG_API_MAX_FIELDS, - fileSize: DEFAULT_CONFIG_API_MAX_UPLOAD_FILE_SIZE, - files: DEFAULT_CONFIG_API_MAX_FILES, - parts: DEFAULT_CONFIG_API_MAX_PARTS, - headerPairs: DEFAULT_CONFIG_API_MAX_HEADERS, - }), - preservePath: true, - fileFilter: this.fileFilter, - }); - } - /** - * Perform the filtering of the request based on the attached file properties and the request - * @param request - HTTP request express object - * @param file - file information - * @param callback - callback function to return the final decision - */ - private readonly fileFilter = ( - request: Request, - file: Express.Multer.File, - callback: FileFilterCallback - ): void => { - if (this.allowedMimeTypes.length === 0 || this.allowedMimeTypes.includes(file.mimetype)) { - callback(null, true); - } else { - callback( - BoomHelpers.unsupportedMediaType( - `Unsupported media type: [${file.mimetype}]. Supported types: [${this.allowedMimeTypes}]`, - request.uuid, - { - source: { - pointer: request.path, - parameter: { body: request.body, query: request.query }, - }, - } - ) - ); - } - }; - /** - * Accept a single file with the name fieldName. The single file will be stored in req.file - * @param fieldName - name of the file - */ - public single(fieldName: string): RequestHandler { - return this.multerWrap(this.instance.single(fieldName)); - } - /** - * Accept an array of files, all with the name fieldName. Optionally error out if more than - * maxCount files are uploaded. The array of files will be stored in req.files - * @param fieldName - name of file - * @param maxCount - maximum number of files - */ - public array(fieldName: string, maxCount?: number): RequestHandler { - return this.multerWrap(this.instance.array(fieldName, maxCount)); - } - /** - * Accept a mix of files, specified by fields. An object with arrays of files will be stored in - * req.files. - * @param fields - array of entries - * - * @example - * - * ``` - * [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }] - * ``` - */ - public fields(fields: readonly multer.Field[]): RequestHandler { - return this.multerWrap(this.instance.fields(fields)); - } - /** - * Accept only text fields. If any file upload is made, error with code "Unexpected field" will - * be issued. - */ - public none(): RequestHandler { - return this.multerWrap(this.instance.none()); - } - /** - * Accepts all files that comes over the wire. An array of files will be stored in req.files. - */ - public any(): RequestHandler { - return this.multerWrap(this.instance.any()); - } - /** - * Wrap the multer middleware functions for error management - * @param middleware - multer middleware handled function to wrap - */ - private multerWrap( - middleware: (request: Request, response: Response, next: NextFunction) => void - ): RequestHandler { - return (request: Request, response: Response, next: NextFunction) => { - middleware(request, response, (error?: Error | MulterError | any) => { - if (error instanceof MulterError) { - const validationError = new Multi(`Errors during form processing`, request.uuid, { - name: 'ValidationError', - }); - const formFormatError = new Crash(`Form error: ${error.message}`, request.uuid, { - name: 'ValidationError', - }); - validationError.push(formFormatError); - next(validationError); - } else if (error) { - next(error); - } else { - next(); - } - }); - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { BoomHelpers, Crash, Multi } from '@mdf.js/crash'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { merge } from 'lodash'; +import multer, { FileFilterCallback, MulterError, Options, StorageEngine } from 'multer'; + +const MEMORY_STORAGE = multer.memoryStorage(); + +export type MulterLimits = Options['limits']; + +// ************************************************************************************************* +// #region Multer limits and configuration default values +/** Max field name size (in bytes) */ +const DEFAULT_CONFIG_API_MAX_FORM_FIELD_SIZE = 100; +/** Max field value size (in bytes) */ +const DEFAULT_CONFIG_API_MAX_FIELD_SIZE = 1048576; +/** Max number of non-file fields */ +const DEFAULT_CONFIG_API_MAX_FIELDS = 100; +/** For multipart forms, the max file size (in bytes)*/ +const DEFAULT_CONFIG_API_MAX_UPLOAD_FILE_SIZE = 1048576 * 1; +/** For multipart forms, the max number of file fields */ +const DEFAULT_CONFIG_API_MAX_FILES = 10; +/** For multipart forms, the max number of parts (fields + files) */ +const DEFAULT_CONFIG_API_MAX_PARTS = 100; +/** For multipart forms, the max number of header key=>value pairs to parse */ +const DEFAULT_CONFIG_API_MAX_HEADERS = 2000; +// #endregion +/** + * Multer middleware wrapping for multipart/from-data + * @remarks WARNING: + * Make sure that you always handle the files that a user uploads. Never add multer as a global + * middleware since a malicious user could upload files to a route that you didn't anticipate. Only + * use this function on routes where you are handling the uploaded files. + */ +export class Multer { + /** Allowed mime types allowed for this multer instance */ + private readonly allowedMimeTypes: string[] = []; + /** Instance of multer */ + private readonly instance: multer.Multer; + /** + * Return a new instance of the multipart/form-data middleware + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static Instance( + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): Multer { + return new Multer(storage, allowedMineTypes, limits); + } + /** + * Return a new instance of the multipart/form-data middleware that accepts a single file with the + * name fieldName. The single file will be stored in req.file + * @param fieldName - name of the file field + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static SingleHandler( + fieldName: string, + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): RequestHandler { + return new Multer(storage, allowedMineTypes, limits).single(fieldName); + } + /** + * Return a new instance of the multipart/form-data middleware that accepts an array of files, all + * with the name fieldName. Optionally error out if more than maxCount files are uploaded. The + * array of files will be stored in req.files + * @param fieldName - name of the file field + * @param maxCount - maximum number of files + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static ArrayHandler( + fieldName: string, + maxCount?: number, + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): RequestHandler { + return new Multer(storage, allowedMineTypes, limits).array(fieldName, maxCount); + } + /** + * Return a new instance of the multipart/form-data middleware that accepts a mix of files, + * specified by fields. An object with arrays of files will be stored in req.files. + * @param fields - array of entries + * + * @example + * + * ``` + * [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }] + * ``` + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static FieldsHandler( + fields: readonly multer.Field[], + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): RequestHandler { + return new Multer(storage, allowedMineTypes, limits).fields(fields); + } + /** + * Return a new instance of the multipart/form-data middleware that accepts only text fields. If + * any file upload is made, error with code "Unexpected field" will be issued. + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static NoneHandler( + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): RequestHandler { + return new Multer(storage, allowedMineTypes, limits).none(); + } + /** + * Return a new instance of the multipart/form-data middleware that accepts all files that comes + * over the wire. An array of files will be stored in req.files. + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + static AnyHandler( + storage?: StorageEngine, + allowedMineTypes?: string | string[], + limits?: MulterLimits + ): RequestHandler { + return new Multer(storage, allowedMineTypes, limits).any(); + } + /** + * Return a new instance of the multipart/form-data middleware + * @param storage - storage engine used for this middleware + * @param allowedMineTypes - Allowed mime types allowed for this multer instance + * @param limits - Limits for the middleware + */ + private constructor( + storage: StorageEngine = MEMORY_STORAGE, + allowedMineTypes: string | string[] = [], + limits: MulterLimits + ) { + if (typeof allowedMineTypes === 'string' || Array.isArray(allowedMineTypes)) { + this.allowedMimeTypes = + typeof allowedMineTypes === 'string' ? [allowedMineTypes] : allowedMineTypes; + } + this.instance = multer({ + storage, + limits: merge(limits, { + fieldNameSize: DEFAULT_CONFIG_API_MAX_FORM_FIELD_SIZE, + fieldSize: DEFAULT_CONFIG_API_MAX_FIELD_SIZE, + fields: DEFAULT_CONFIG_API_MAX_FIELDS, + fileSize: DEFAULT_CONFIG_API_MAX_UPLOAD_FILE_SIZE, + files: DEFAULT_CONFIG_API_MAX_FILES, + parts: DEFAULT_CONFIG_API_MAX_PARTS, + headerPairs: DEFAULT_CONFIG_API_MAX_HEADERS, + }), + preservePath: true, + fileFilter: this.fileFilter, + }); + } + /** + * Perform the filtering of the request based on the attached file properties and the request + * @param request - HTTP request express object + * @param file - file information + * @param callback - callback function to return the final decision + */ + private readonly fileFilter = ( + request: Request, + file: Express.Multer.File, + callback: FileFilterCallback + ): void => { + if (this.allowedMimeTypes.length === 0 || this.allowedMimeTypes.includes(file.mimetype)) { + callback(null, true); + } else { + callback( + BoomHelpers.unsupportedMediaType( + `Unsupported media type: [${file.mimetype}]. Supported types: [${this.allowedMimeTypes}]`, + request.uuid, + { + source: { + pointer: request.path, + parameter: { body: request.body, query: request.query }, + }, + } + ) + ); + } + }; + /** + * Accept a single file with the name fieldName. The single file will be stored in req.file + * @param fieldName - name of the file + */ + public single(fieldName: string): RequestHandler { + return this.multerWrap(this.instance.single(fieldName)); + } + /** + * Accept an array of files, all with the name fieldName. Optionally error out if more than + * maxCount files are uploaded. The array of files will be stored in req.files + * @param fieldName - name of file + * @param maxCount - maximum number of files + */ + public array(fieldName: string, maxCount?: number): RequestHandler { + return this.multerWrap(this.instance.array(fieldName, maxCount)); + } + /** + * Accept a mix of files, specified by fields. An object with arrays of files will be stored in + * req.files. + * @param fields - array of entries + * + * @example + * + * ``` + * [ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }] + * ``` + */ + public fields(fields: readonly multer.Field[]): RequestHandler { + return this.multerWrap(this.instance.fields(fields)); + } + /** + * Accept only text fields. If any file upload is made, error with code "Unexpected field" will + * be issued. + */ + public none(): RequestHandler { + return this.multerWrap(this.instance.none()); + } + /** + * Accepts all files that comes over the wire. An array of files will be stored in req.files. + */ + public any(): RequestHandler { + return this.multerWrap(this.instance.any()); + } + /** + * Wrap the multer middleware functions for error management + * @param middleware - multer middleware handled function to wrap + */ + private multerWrap( + middleware: (request: Request, response: Response, next: NextFunction) => void + ): RequestHandler { + return (request: Request, response: Response, next: NextFunction) => { + middleware(request, response, (error?: Error | MulterError | any) => { + if (error instanceof MulterError) { + const validationError = new Multi(`Errors during form processing`, request.uuid, { + name: 'ValidationError', + }); + const formFormatError = new Crash(`Form error: ${error.message}`, request.uuid, { + name: 'ValidationError', + }); + validationError.push(formFormatError); + next(validationError); + } else if (error) { + next(error); + } else { + next(); + } + }); + }; + } +} + diff --git a/packages/api/middlewares/src/rateLimiter/RateLimitConfig.i.ts b/packages/api/middlewares/src/rateLimiter/RateLimitConfig.i.ts index a2fa6ff6..53ef82a0 100644 --- a/packages/api/middlewares/src/rateLimiter/RateLimitConfig.i.ts +++ b/packages/api/middlewares/src/rateLimiter/RateLimitConfig.i.ts @@ -1,17 +1,24 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -interface RateLimitEntry { - [x: string]: { - maxRequests: number; - timeWindow: number; - }; -} -export interface RateLimitConfig { - enabled: boolean; - rates: Array; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Rate limit entry */ +export interface RateLimitEntry { + [x: string]: { + /** Maximum number of requests */ + maxRequests: number; + /** Time window in seconds */ + timeWindow: number; + }; +} + +/** Rate limit configuration */ +export interface RateLimitConfig { + /** Enable rate limiting */ + enabled: boolean; + /** Rate limits */ + rates: Array; +} diff --git a/packages/api/middlewares/src/rateLimiter/rateLimiter.ts b/packages/api/middlewares/src/rateLimiter/rateLimiter.ts index e096ec00..92ac5294 100644 --- a/packages/api/middlewares/src/rateLimiter/rateLimiter.ts +++ b/packages/api/middlewares/src/rateLimiter/rateLimiter.ts @@ -1,71 +1,72 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { BoomHelpers } from '@mdf.js/crash'; -import { RequestHandler } from 'express'; -import RateLimit from 'express-rate-limit'; -import { RateLimitConfig } from './RateLimitConfig.i'; -const MINUTE = 60000; - -/** RateLimiter class manages the API requests rate limits */ -export class RateLimiter { - /** Rate limiters configuration */ - private readonly config: RateLimitConfig; - /** Flag that indicates that the rates limiters are enabled */ - private readonly enable: boolean; - /** Rate limiters map */ - private readonly RateLimiterMap: Map = new Map(); - /** Empty rate limiter */ - private readonly emptyMiddleware: RequestHandler = (req, res, next) => { - next(); - }; - /** - * Create an instance of RateLimiter - * @param configuration - Rate limiters configuration - */ - constructor(configuration: RateLimitConfig) { - this.config = configuration; - this.enable = this.config.enabled; - if (this.config.rates && this.config.rates.length) { - this.config.rates.forEach((rate: any) => { - const label = Object.keys(rate)[0]; - const handler = this.requestHandler(rate[label].maxRequests, rate[label].timeWindow); - this.RateLimiterMap.set(label, handler); - }); - } else { - this.enable = false; - } - } - /** Create a request handler for a rate limiter */ - private requestHandler(maxRequests: number, timeWindow: number): RequestHandler { - return RateLimit({ - max: maxRequests, - windowMs: timeWindow * MINUTE, - headers: true, - handler: (req, res, next) => { - next( - BoomHelpers.tooManyRequests('Too many requests, please try again later', req.uuid, { - source: { - pointer: req.path, - parameter: req.body, - }, - }) - ); - }, - skipFailedRequests: false, - skipSuccessfulRequests: false, - }); - } - /** Get the request handler */ - public get(label: string): RequestHandler { - const result = this.RateLimiterMap.get(label); - if (result && this.enable) { - return result; - } else { - return this.emptyMiddleware; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { BoomHelpers } from '@mdf.js/crash'; +import { RequestHandler } from 'express'; +import RateLimit from 'express-rate-limit'; +import { RateLimitConfig } from './RateLimitConfig.i'; +const MINUTE = 60000; + +/** RateLimiter class manages the API requests rate limits */ +export class RateLimiter { + /** Rate limiters configuration */ + private readonly config: RateLimitConfig; + /** Flag that indicates that the rates limiters are enabled */ + private readonly enable: boolean; + /** Rate limiters map */ + private readonly RateLimiterMap: Map = new Map(); + /** Empty rate limiter */ + private readonly emptyMiddleware: RequestHandler = (req, res, next) => { + next(); + }; + /** + * Create an instance of RateLimiter + * @param configuration - Rate limiters configuration + */ + constructor(configuration: RateLimitConfig) { + this.config = configuration; + this.enable = this.config.enabled; + if (this.config.rates?.length) { + this.config.rates.forEach((rate: any) => { + const label = Object.keys(rate)[0]; + const handler = this.requestHandler(rate[label].maxRequests, rate[label].timeWindow); + this.RateLimiterMap.set(label, handler); + }); + } else { + this.enable = false; + } + } + /** Create a request handler for a rate limiter */ + private requestHandler(maxRequests: number, timeWindow: number): RequestHandler { + return RateLimit({ + max: maxRequests, + windowMs: timeWindow * MINUTE, + headers: true, + handler: (req, res, next) => { + next( + BoomHelpers.tooManyRequests('Too many requests, please try again later', req.uuid, { + source: { + pointer: req.path, + parameter: req.body, + }, + }) + ); + }, + skipFailedRequests: false, + skipSuccessfulRequests: false, + }); + } + /** Get the request handler */ + public get(label: string): RequestHandler { + const result = this.RateLimiterMap.get(label); + if (result && this.enable) { + return result; + } else { + return this.emptyMiddleware; + } + } +} + diff --git a/packages/api/openc2-core/README.md b/packages/api/openc2-core/README.md index cb225493..6b4ba104 100644 --- a/packages/api/openc2-core/README.md +++ b/packages/api/openc2-core/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/openc2-core/package.json b/packages/api/openc2-core/package.json index ff561f72..2eaffc3d 100644 --- a/packages/api/openc2-core/package.json +++ b/packages/api/openc2-core/package.json @@ -36,16 +36,15 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "@types/express": "^4.17.21", - "express": "^4.21.1", + "express": "^4.21.2", "lodash": "^4.17.21", - "tslib": "^2.7.0", - "uuid": "^10.0.0" + "tslib": "^2.8.1", + "uuid": "^11.0.3" }, "devDependencies": { "@mdf.js/middlewares": "*", "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0", + "@types/lodash": "^4.17.13", "supertest": "^7.0.0" }, "engines": { diff --git a/packages/api/openc2-core/src/Router/oc2.controller.ts b/packages/api/openc2-core/src/Router/oc2.controller.ts index 4fdb37cd..8753b56f 100644 --- a/packages/api/openc2-core/src/Router/oc2.controller.ts +++ b/packages/api/openc2-core/src/Router/oc2.controller.ts @@ -1,139 +1,139 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Boom, BoomHelpers } from '@mdf.js/crash'; -import { NextFunction, Request, Response } from 'express'; -import { Control } from '../types'; -import { Service } from './oc2.service'; - -/** Controller class */ -export class Controller { - /** - * Create an instance of Controller class - * @param service - service instance - */ - constructor(private readonly service: Service) {} - /** - * Return array of messages, pendingJobs and jobs used as fifo registry - * @param request - HTTP request express object - * @param response - HTTP response express object - * @param next - Next express middleware function - */ - public query(request: Request, response: Response, next: NextFunction): void { - let selector: Promise | undefined; - const param = request.params['id']; - switch (param) { - case 'jobs': - selector = this.service.jobs(); - break; - case 'messages': - selector = this.service.messages(); - break; - case 'pendingJobs': - selector = this.service.pendingJobs(); - break; - default: - selector = undefined; - } - if (!selector) { - next(BoomHelpers.badRequest(`Invalid parameter ${param}`, request.uuid)); - } else { - selector - .then(result => { - if (result.length > 0) { - response.status(200).json(result); - } else { - response.status(204).send(); - } - }) - .catch(next); - } - } - /** - * Execute a command over the producer or consumer - * @param request - HTTP request express object - * @param response - HTTP response express object - * @param next - Next express middleware function - * @returns - response message - */ - public command(request: Request, response: Response, next: NextFunction): void { - this.service - .command(request.body) - .then(result => { - if (Array.isArray(result)) { - if (result.length > 0) { - response.status(200).json(result); - } else { - response.status(204).send(); - } - } else if (result) { - if ( - result.status === Control.StatusCode.Processing || - result.status === Control.StatusCode.OK - ) { - response.status(200).json(result); - } else { - throw this.OC2ResponseToHTTPResponse(result); - } - } else { - response.status(204).send(); - } - }) - .catch(next); - } - /** - * Return a Boom object from a OC2 response - * @param response - OC2 response - * @returns - Boom object - */ - private OC2ResponseToHTTPResponse(response: Control.ResponseMessage): Boom { - switch (response.status) { - case Control.StatusCode.BadRequest: - return BoomHelpers.badRequest( - response.content.status_text || 'Bad OC2 Request', - response.request_id, - { info: response } - ); - case Control.StatusCode.Unauthorized: - return BoomHelpers.unauthorized( - response.content.status_text || 'Unauthorized OC2 Request', - response.request_id, - { info: response } - ); - case Control.StatusCode.Forbidden: - return BoomHelpers.forbidden( - response.content.status_text || 'Forbidden OC2 Request', - response.request_id, - { info: response } - ); - case Control.StatusCode.NotFound: - return BoomHelpers.notFound( - response.content.status_text || 'Not Found OC2 Request', - response.request_id, - { info: response } - ); - case Control.StatusCode.NotImplemented: - return BoomHelpers.notImplemented( - response.content.status_text || 'Not Implemented OC2 Request', - response.request_id, - { info: response } - ); - case Control.StatusCode.ServiceUnavailable: - return BoomHelpers.serverUnavailable( - response.content.status_text || 'Service Unavailable OC2 Request', - response.request_id, - { info: response } - ); - default: - return BoomHelpers.internalServerError( - response.content.status_text || 'Unknown OC2 response status code', - response.request_id, - { info: response } - ); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Boom, BoomHelpers } from '@mdf.js/crash'; +import { NextFunction, Request, Response } from 'express'; +import { Control } from '../types'; +import { Service } from './oc2.service'; + +/** Controller class */ +export class Controller { + /** + * Create an instance of Controller class + * @param service - service instance + */ + constructor(private readonly service: Service) {} + /** + * Return array of messages, pendingJobs and jobs used as fifo registry + * @param request - HTTP request express object + * @param response - HTTP response express object + * @param next - Next express middleware function + */ + public query(request: Request, response: Response, next: NextFunction): void { + let selector: Promise | undefined; + const param = request.params['id']; + switch (param) { + case 'jobs': + selector = this.service.jobs(); + break; + case 'messages': + selector = this.service.messages(); + break; + case 'pendingJobs': + selector = this.service.pendingJobs(); + break; + default: + selector = undefined; + } + if (!selector) { + next(BoomHelpers.badRequest(`Invalid parameter ${param}`, request.uuid)); + } else { + selector + .then(result => { + if (result.length > 0) { + response.status(200).json(result); + } else { + response.status(204).send(); + } + }) + .catch(next); + } + } + /** + * Execute a command over the producer or consumer + * @param request - HTTP request express object + * @param response - HTTP response express object + * @param next - Next express middleware function + * @returns - response message + */ + public command(request: Request, response: Response, next: NextFunction): void { + this.service + .command(request.body) + .then(result => { + if (Array.isArray(result)) { + if (result.length > 0) { + response.status(200).json(result); + } else { + response.status(204).send(); + } + } else if (result) { + if ( + result.status === Control.StatusCode.Processing || + result.status === Control.StatusCode.OK + ) { + response.status(200).json(result); + } else { + throw this.OC2ResponseToHTTPResponse(result); + } + } else { + response.status(204).send(); + } + }) + .catch(next); + } + /** + * Return a Boom object from a OC2 response + * @param response - OC2 response + * @returns - Boom object + */ + private OC2ResponseToHTTPResponse(response: Control.ResponseMessage): Boom { + switch (response.status) { + case Control.StatusCode.BadRequest: + return BoomHelpers.badRequest( + response.content.status_text ?? 'Bad OC2 Request', + response.request_id, + { info: response } + ); + case Control.StatusCode.Unauthorized: + return BoomHelpers.unauthorized( + response.content.status_text ?? 'Unauthorized OC2 Request', + response.request_id, + { info: response } + ); + case Control.StatusCode.Forbidden: + return BoomHelpers.forbidden( + response.content.status_text ?? 'Forbidden OC2 Request', + response.request_id, + { info: response } + ); + case Control.StatusCode.NotFound: + return BoomHelpers.notFound( + response.content.status_text ?? 'Not Found OC2 Request', + response.request_id, + { info: response } + ); + case Control.StatusCode.NotImplemented: + return BoomHelpers.notImplemented( + response.content.status_text ?? 'Not Implemented OC2 Request', + response.request_id, + { info: response } + ); + case Control.StatusCode.ServiceUnavailable: + return BoomHelpers.serverUnavailable( + response.content.status_text ?? 'Service Unavailable OC2 Request', + response.request_id, + { info: response } + ); + default: + return BoomHelpers.internalServerError( + response.content.status_text ?? 'Unknown OC2 response status code', + response.request_id, + { info: response } + ); + } + } +} diff --git a/packages/api/openc2-core/src/components/Consumer/Consumer.ts b/packages/api/openc2-core/src/components/Consumer/Consumer.ts index c23083a6..993112f3 100644 --- a/packages/api/openc2-core/src/components/Consumer/Consumer.ts +++ b/packages/api/openc2-core/src/components/Consumer/Consumer.ts @@ -1,391 +1,388 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Health, Jobs } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { Accessors, Checkers, Helpers } from '../../helpers'; -import { - CommandJobHandler, - CommandJobRequest, - ConsumerAdapter, - ConsumerOptions, - Control, - OnCommandHandler, - Resolver, - ResolverEntry, -} from '../../types'; -import { Component } from '../Component'; -import { AdapterWrapper } from './core'; - -export declare interface Consumer { - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - on(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - addListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - once(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - off(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - removeListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `error` event - */ - removeAllListeners(event?: 'error'): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - on(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - addListener(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - once(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - off(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - removeListener(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `command` event, emitted when a new command is received - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - on(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Add a listener for the `command` event, emitted when a new command is received - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - addListener(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Add a listener for the `command` event, emitted when a new command is received. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - once(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes the specified listener from the listener array for the `command` event. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - off(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes the specified listener from the listener array for the `command` event. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - removeListener(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `command` event - */ - removeAllListeners(event?: 'command'): this; -} -export class Consumer extends Component { - /** - * Regular OpenC2 consumer implementation. - * @param adapter - transport adapter - * @param options - configuration options - */ - constructor(adapter: ConsumerAdapter, options: ConsumerOptions) { - super(new AdapterWrapper(adapter, options.retryOptions), options); - this._router.on('command', this.onCommandHandler); - // Stryker disable next-line all - this.logger.debug(`OpenC2 Consumer created - [${options.id}]`); - this.validateResolver(options); - } - /** - * Validate the resolver map if exists - * @param options - configuration options - * @returns - */ - private validateResolver(options: ConsumerOptions): void { - if (!options.resolver) { - return; - } - for (const entry of Object.keys(options.resolver)) { - const { actionType, namespace, target } = this.validateResolverEntry(entry as ResolverEntry); - if (!this.options.actionTargetPairs[actionType]) { - throw new Crash(`Invalid resolver entry, action type not supported: ${entry}`, { - name: 'ValidationError', - }); - } else if (!this.options.actionTargetPairs[actionType]?.includes(`${namespace}:${target}`)) { - throw new Crash(`Invalid resolver entry, target not supported: ${entry}`, { - name: 'ValidationError', - }); - } else { - return; - } - } - } - /** - * Validate a resolver entry format - * @param entry - resolver entry to validate - */ - private validateResolverEntry(entry: ResolverEntry): { - actionType: Control.ActionType; - namespace: Control.Namespace; - target: string; - } { - const { 0: actionType, 1: namespace, 2: target } = entry.split(':'); - if (!actionType || !namespace || !target) { - throw new Crash(`Invalid resolver entry, invalid format: ${entry}`, { - name: 'ValidationError', - }); - } else if (!Control.ACTION_TYPES.includes(actionType as Control.ActionType)) { - throw new Crash(`Invalid resolver entry, unknown action type: ${actionType}`, { - name: 'ValidationError', - }); - } else { - return { - actionType: actionType as Control.ActionType, - namespace: namespace as Control.Namespace, - target, - }; - } - } - /** Consumer actuators */ - public get actuator(): string[] | undefined { - return this.options.actuator; - } - public set actuator(value: string[] | undefined) { - this.options.actuator = value; - } - /** Consumer profiles */ - public get profiles(): string[] | undefined { - return this.options.profiles; - } - public set profiles(value: string[] | undefined) { - this.options.profiles = value; - } - /** Consumer pairs */ - public get pairs(): Control.ActionTargetPairs { - return this.options.actionTargetPairs; - } - public set pairs(value: Control.ActionTargetPairs) { - this.options.actionTargetPairs = value; - } - /** Initialize the OpenC2 component */ - protected startup(): Promise { - return this.adapter.subscribe(this.onCommandHandler); - } - /** Shutdown the OpenC2 component */ - protected shutdown(): Promise { - return this.adapter.unsubscribe(this.onCommandHandler); - } - /** - * Process incoming command message from the adapter - * @param incomingMessage - incoming message - * @param done - callback to return the response - */ - private readonly onCommandHandler: OnCommandHandler = ( - incomingMessage: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ): void => { - this.processCommand(incomingMessage) - .then(result => done(undefined, result)) - .catch(error => done(error, undefined)); - }; - /** - * Process incoming message and return a response if the message is a command that should be - * responded - * @param incomingMessage - incoming message - */ - private readonly processCommand = async ( - incomingMessage: Control.CommandMessage - ): Promise => { - try { - const message = Checkers.isValidCommandSync(incomingMessage, this.componentId); - // Stryker disable next-line all - this.logger.debug(`New message from ${message.from} - ${message.request_id}`); - if (Checkers.isCommandToInstance(message, this.options.id)) { - return this.classifyCommand(message); - } - // Stryker disable next-line all - this.logger.debug(`${message.request_id} is not a command for this instance`); - return undefined; - } catch (rawError) { - const error = Crash.from(rawError); - const crashError = new Crash( - `Error processing incoming command message from control chanel: ${error.message}`, - this.componentId, - { cause: error } - ); - this.onErrorHandler(crashError); - throw crashError; - } - }; - /** - * Process incoming command message and select a default response or emit a new job to execute - * the command - * @param command - command message to be processed - */ - private async classifyCommand( - message: Control.CommandMessage - ): Promise { - const defaultResponse = Checkers.hasDefaultResponse(message, this.options); - if (defaultResponse) { - this.register.push(message); - // Stryker disable next-line all - this.logger.debug(`${message.request_id} has default response`); - return defaultResponse; - } else { - const resolver = this.getResolver(message); - if (resolver) { - return this.resolveCommand(resolver, message); - } else { - return this.emitCommandJob(this.createJobFromCommand(message)); - } - } - } - /** - * Resolve a command message using a resolver - * @param resolver - resolver function to be executed - * @param message - message to be processed - * @returns - */ - private async resolveCommand( - resolver: Resolver, - message: Control.CommandMessage - ): Promise { - try { - const target = Accessors.getTargetFromCommand(message.content) as keyof Control.Target; - const response = await resolver(message.content.target[target]); - this.logger.info(`Command was resolved successfully`); - const result = - response !== undefined && response !== null ? { [target]: response } : undefined; - return Helpers.ok(message, this.options.id, result); - } catch (rawError) { - const cause = Crash.from(rawError); - // Stryker disable next-line all - this.logger.error(`Command was resolved with errors: ${cause.message}`); - return Helpers.internalError(message, this.options.id, cause.trace().join(',')); - } - } - /** - * Execute a job and wait for the resolution - * @param message - message to be processed as a job for upper layers - */ - private async emitCommandJob( - job: CommandJobHandler - ): Promise { - this.register.push(job); - return new Promise(resolve => { - const onCommandJobDone = (uuid: string) => { - let response: Control.ResponseMessage | undefined; - const commandJob = this.register.delete(uuid); - if (!commandJob) { - response = Helpers.internalError( - job.data, - this.options.id, - `Job ${uuid} is not registered in the consumer. It can't be processed` - ); - } else if (commandJob.hasErrors) { - // Stryker disable next-line all - this.logger.error(`Job ${commandJob.jobUserId} was finished with errors`); - response = Helpers.internalError( - commandJob.data, - this.options.id, - `${job.errors?.toString()}` - ); - } else { - // Stryker disable next-line all - this.logger.info(`Job ${commandJob.jobUserId} was finished successfully`); - response = Helpers.ok(commandJob.data, this.options.id); - } - resolve(response); - }; - job.once('done', onCommandJobDone); - this.emit('command', job); - }); - } - /** - * Create a job from a command message - * @param message - command message - * @returns - */ - private createJobFromCommand(message: Control.CommandMessage): CommandJobHandler { - // Stryker disable next-line all - this.logger.info(`Job - ${message.content.action}-${Object.keys(message.content.target)[0]}`); - const jobRequest: CommandJobRequest = { - jobUserId: message.request_id, - data: message, - type: 'command', - options: { - headers: { duration: Accessors.getDelayFromCommandMessage(message) }, - }, - }; - return new Jobs.JobHandler(jobRequest); - } - /** - * Check if there is a resolver function for the command in the resolver map - * @param message - message to be processed - */ - private getResolver(message: Control.CommandMessage): Resolver | undefined { - const target = Accessors.getTargetFromCommand(message.content); - const entry = `${message.content.action}:${target}` as ResolverEntry; - if (this.options.resolver && this.options.resolver[entry]) { - return this.options.resolver[entry]; - } - return undefined; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Health, Jobs } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { Accessors, Checkers, Helpers } from '../../helpers'; +import { + CommandJobHandler, + CommandJobRequest, + ConsumerAdapter, + ConsumerOptions, + Control, + OnCommandHandler, + Resolver, + ResolverEntry, +} from '../../types'; +import { Component } from '../Component'; +import { AdapterWrapper } from './core'; + +export declare interface Consumer { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + addListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + once(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + off(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + removeListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `error` event + */ + removeAllListeners(event?: 'error'): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + addListener(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + once(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + off(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + removeListener(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `command` event, emitted when a new command is received + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + on(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Add a listener for the `command` event, emitted when a new command is received + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + addListener(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Add a listener for the `command` event, emitted when a new command is received. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + once(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes the specified listener from the listener array for the `command` event. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + off(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes the specified listener from the listener array for the `command` event. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + removeListener(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `command` event + */ + removeAllListeners(event?: 'command'): this; +} +export class Consumer extends Component { + /** + * Regular OpenC2 consumer implementation. + * @param adapter - transport adapter + * @param options - configuration options + */ + constructor(adapter: ConsumerAdapter, options: ConsumerOptions) { + super(new AdapterWrapper(adapter, options.retryOptions), options); + this._router.on('command', this.onCommandHandler); + // Stryker disable next-line all + this.logger.debug(`OpenC2 Consumer created - [${options.id}]`); + this.validateResolver(options); + } + /** + * Validate the resolver map if exists + * @param options - configuration options + * @returns + */ + private validateResolver(options: ConsumerOptions): void { + if (!options.resolver) { + return; + } + for (const entry of Object.keys(options.resolver)) { + const { actionType, namespace, target } = this.validateResolverEntry(entry as ResolverEntry); + if (!this.options.actionTargetPairs[actionType]) { + throw new Crash(`Invalid resolver entry, action type not supported: ${entry}`, { + name: 'ValidationError', + }); + } else if (!this.options.actionTargetPairs[actionType]?.includes(`${namespace}:${target}`)) { + throw new Crash(`Invalid resolver entry, target not supported: ${entry}`, { + name: 'ValidationError', + }); + } else { + return; + } + } + } + /** + * Validate a resolver entry format + * @param entry - resolver entry to validate + */ + private validateResolverEntry(entry: ResolverEntry): { + actionType: Control.ActionType; + namespace: Control.Namespace; + target: string; + } { + const { 0: actionType, 1: namespace, 2: target } = entry.split(':'); + if (!actionType || !namespace || !target) { + throw new Crash(`Invalid resolver entry, invalid format: ${entry}`, { + name: 'ValidationError', + }); + } else if (!Control.ACTION_TYPES.includes(actionType as Control.ActionType)) { + throw new Crash(`Invalid resolver entry, unknown action type: ${actionType}`, { + name: 'ValidationError', + }); + } else { + return { + actionType: actionType as Control.ActionType, + namespace: namespace as Control.Namespace, + target, + }; + } + } + /** Consumer actuators */ + public get actuator(): string[] | undefined { + return this.options.actuator; + } + public set actuator(value: string[] | undefined) { + this.options.actuator = value; + } + /** Consumer profiles */ + public get profiles(): string[] | undefined { + return this.options.profiles; + } + public set profiles(value: string[] | undefined) { + this.options.profiles = value; + } + /** Consumer pairs */ + public get pairs(): Control.ActionTargetPairs { + return this.options.actionTargetPairs; + } + public set pairs(value: Control.ActionTargetPairs) { + this.options.actionTargetPairs = value; + } + /** Initialize the OpenC2 component */ + protected startup(): Promise { + return this.adapter.subscribe(this.onCommandHandler); + } + /** Shutdown the OpenC2 component */ + protected shutdown(): Promise { + return this.adapter.unsubscribe(this.onCommandHandler); + } + /** + * Process incoming command message from the adapter + * @param incomingMessage - incoming message + * @param done - callback to return the response + */ + private readonly onCommandHandler: OnCommandHandler = ( + incomingMessage: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ): void => { + this.processCommand(incomingMessage) + .then(result => done(undefined, result)) + .catch(error => done(error, undefined)); + }; + /** + * Process incoming message and return a response if the message is a command that should be + * responded + * @param incomingMessage - incoming message + */ + private readonly processCommand = async ( + incomingMessage: Control.CommandMessage + ): Promise => { + try { + const message = Checkers.isValidCommandSync(incomingMessage, this.componentId); + // Stryker disable next-line all + this.logger.debug(`New message from ${message.from} - ${message.request_id}`); + if (Checkers.isCommandToInstance(message, this.options.id)) { + return this.classifyCommand(message); + } + // Stryker disable next-line all + this.logger.debug(`${message.request_id} is not a command for this instance`); + return undefined; + } catch (rawError) { + const error = Crash.from(rawError); + const crashError = new Crash( + `Error processing incoming command message from control chanel: ${error.message}`, + this.componentId, + { cause: error } + ); + this.onErrorHandler(crashError); + throw crashError; + } + }; + /** + * Process incoming command message and select a default response or emit a new job to execute + * the command + * @param command - command message to be processed + */ + private async classifyCommand( + message: Control.CommandMessage + ): Promise { + const defaultResponse = Checkers.hasDefaultResponse(message, this.options); + if (defaultResponse) { + this.register.push(message); + // Stryker disable next-line all + this.logger.debug(`${message.request_id} has default response`); + return defaultResponse; + } else { + const resolver = this.getResolver(message); + if (resolver) { + return this.resolveCommand(resolver, message); + } else { + return this.emitCommandJob(this.createJobFromCommand(message)); + } + } + } + /** + * Resolve a command message using a resolver + * @param resolver - resolver function to be executed + * @param message - message to be processed + * @returns + */ + private async resolveCommand( + resolver: Resolver, + message: Control.CommandMessage + ): Promise { + try { + const target = Accessors.getTargetFromCommand(message.content) as keyof Control.Target; + const response = await resolver(message.content.target[target]); + this.logger.info(`Command was resolved successfully`); + const result = + response !== undefined && response !== null ? { [target]: response } : undefined; + return Helpers.ok(message, this.options.id, result); + } catch (rawError) { + const cause = Crash.from(rawError); + // Stryker disable next-line all + this.logger.error(`Command was resolved with errors: ${cause.message}`); + return Helpers.internalError(message, this.options.id, cause.trace().join(',')); + } + } + /** + * Execute a job and wait for the resolution + * @param message - message to be processed as a job for upper layers + */ + private async emitCommandJob( + job: CommandJobHandler + ): Promise { + this.register.push(job); + return new Promise(resolve => { + const onCommandJobDone = (uuid: string) => { + let response: Control.ResponseMessage | undefined; + const commandJob = this.register.delete(uuid); + if (!commandJob) { + response = Helpers.internalError( + job.data, + this.options.id, + `Job ${uuid} is not registered in the consumer. It can't be processed` + ); + } else if (commandJob.hasErrors) { + // Stryker disable next-line all + this.logger.error(`Job ${commandJob.jobUserId} was finished with errors`); + response = Helpers.internalError( + commandJob.data, + this.options.id, + `${job.errors?.toString()}` + ); + } else { + // Stryker disable next-line all + this.logger.info(`Job ${commandJob.jobUserId} was finished successfully`); + response = Helpers.ok(commandJob.data, this.options.id); + } + resolve(response); + }; + job.once('done', onCommandJobDone); + this.emit('command', job); + }); + } + /** + * Create a job from a command message + * @param message - command message + * @returns + */ + private createJobFromCommand(message: Control.CommandMessage): CommandJobHandler { + // Stryker disable next-line all + this.logger.info(`Job - ${message.content.action}-${Object.keys(message.content.target)[0]}`); + const jobRequest: CommandJobRequest = { + jobUserId: message.request_id, + data: message, + type: 'command', + options: { + headers: { duration: Accessors.getDelayFromCommandMessage(message) }, + }, + }; + return new Jobs.JobHandler(jobRequest); + } + /** + * Check if there is a resolver function for the command in the resolver map + * @param message - message to be processed + */ + private getResolver(message: Control.CommandMessage): Resolver | undefined { + const target = Accessors.getTargetFromCommand(message.content); + const entry = `${message.content.action}:${target}` as ResolverEntry; + return this.options.resolver?.[entry]; + } +} diff --git a/packages/api/openc2-core/src/components/Gateway/Gateway.ts b/packages/api/openc2-core/src/components/Gateway/Gateway.ts index 895ca64f..3801761b 100644 --- a/packages/api/openc2-core/src/components/Gateway/Gateway.ts +++ b/packages/api/openc2-core/src/components/Gateway/Gateway.ts @@ -1,416 +1,417 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { overallStatus } from '@mdf.js/core/dist/Health'; -import { Crash, Links } from '@mdf.js/crash'; -import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; -import EventEmitter from 'events'; -import express from 'express'; -import { cloneDeep } from 'lodash'; -import { v4 } from 'uuid'; -import { Router } from '../../Router'; -import { Accessors } from '../../helpers'; -import { HealthWrapper, Registry } from '../../modules'; -import { - CommandJobHandler, - ConsumerAdapter, - Control, - GatewayOptions, - ProducerAdapter, -} from '../../types'; -import { Consumer } from '../Consumer'; -import { Producer } from '../Producer'; -import { ConsumerMap } from '../Producer/ConsumerMap'; - -const MIN_LOOKUP_INTERVAL = 10000; -const AGING_INTERVAL_FACTOR = 3; -const MAX_AGED_FACTOR = 3; -const MIN_GATEWAY_DELAY = 1000; - -interface GatewayTimers { - lookupInterval: number; - lookupTimeout: number; - agingInterval: number; - maxAge: number; - delay: number; -} -export declare interface Gateway { - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - on(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - addListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - once(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - off(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - removeListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `error` event - */ - removeAllListeners(event?: 'error'): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - on(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - addListener(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - once(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - off(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - removeListener(event: 'status', listener: (status: Health.Status) => void): this; -} - -export class Gateway extends EventEmitter implements Layer.App.Service { - /** Component identification */ - public readonly componentId: string = v4(); - /** Component commands and message register */ - protected readonly register: Registry; - /** Health wrapper instance */ - private readonly health: HealthWrapper; - /** Logger instance */ - private readonly logger: LoggerInstance; - /** Component started flag */ - private started: boolean; - /** Upstream Consumer */ - private readonly consumer: Consumer; - /** Downstream Producer */ - private readonly producer: Producer; - /** Registry router */ - private readonly _router: Router; - /** - * Regular OpenC2 gateway implementation. - * @param upstream - upstream consumer adapter interface - * @param downstream - downstream producer adapter interface - * @param options - configuration options - */ - constructor( - upstream: ConsumerAdapter, - downstream: ProducerAdapter, - private readonly options: GatewayOptions - ) { - super(); - this.logger = SetContext( - this.options.logger ?? new DebugLogger(`mdf:oc2:gateway:${this.name}`), - this.constructor.name, - this.componentId - ); - this.register = - this.options.registry ?? - new Registry(this.options.id, this.options.maxInactivityTime, this.options.registerLimit); - this._router = new Router(this.register); - this.consumer = new Consumer(upstream, { ...this.options, registry: this.register }); - this.producer = new Producer(downstream, { - ...this.options, - ...(this.options.bypassLookupIntervalChecks ? {} : this.checkLookupTimes(this.options)), - registry: this.register, - }); - this.health = new HealthWrapper(this.options.id, [this.consumer, this.producer]); - this.consumer.on('command', this.onUpstreamCommandHandler); - this.producer.consumerMap.on('updated', this.updateConsumerOptions); - this.started = false; - // Stryker disable next-line all - this.logger.debug(`OpenC2 Gateway created - [${options.id}]`); - } - /** Component name */ - public get name(): string { - return this.options.id; - } - /** Component status */ - public get status(): Health.Status { - return overallStatus(this.health.checks); - } - /** - * Return the status of the Consumer in a standard format - * @returns _check object_ as defined in the draft standard - * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 - */ - public get checks(): Health.Checks { - return this.health.checks; - } - /** Return an Express router with access to errors registry */ - public get router(): express.Router { - return this._router.router; - } - /** Return links offered by this service */ - public get links(): Links { - return { - openc2: { - jobs: '/openc2/jobs', - pendingJobs: '/openc2/pendingJobs', - messages: '/openc2/messages', - }, - }; - } - /** Connect the OpenC2 underlayer component and perform the startup of the component */ - public start(): Promise { - if (this.started) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - this.producer - .start() - .then(() => this.consumer.start()) - .then(() => { - this.health.on('error', this.onErrorHandler); - this.health.on('status', this.onStatusHandler); - }) - .then(() => { - this.started = true; - resolve(); - }) - .catch(reject); - }); - } - /** Disconnect the OpenC2 underlayer component and perform the startup of the component */ - public stop(): Promise { - if (!this.started) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - this.health.off('error', this.onErrorHandler); - this.health.off('status', this.onStatusHandler); - this.consumer - .stop() - .then(() => this.producer.stop()) - .then(() => { - this.started = false; - resolve(); - }) - .catch(reject); - }); - } - /** Close the OpenC2 gateway */ - public close(): Promise { - return this.stop(); - } - /** Consumer Map */ - public get consumerMap(): ConsumerMap { - return this.producer.consumerMap; - } - /** - * Check the lookup time fix them if area wrong - * @param options - configuration options - * @returns - */ - private checkLookupTimes(options: GatewayOptions): GatewayTimers { - const lookupInterval = this.isHigherThan(MIN_LOOKUP_INTERVAL, options.lookupInterval); - const lookupTimeout = this.isLowerThan(lookupInterval / 2, options.lookupTimeout); - const agingInterval = this.isHigherThan( - lookupInterval * AGING_INTERVAL_FACTOR, - options.agingInterval - ); - const maxAge = this.isHigherThan(agingInterval * MAX_AGED_FACTOR, options.maxAge); - const delay = this.isHigherThan(MIN_GATEWAY_DELAY, options.delay); - const selectedOptions = { - lookupInterval, - lookupTimeout, - agingInterval, - maxAge, - delay, - }; - // Stryker disable next-line all - this.logger.debug(`Gateway options ${JSON.stringify(selectedOptions, null, 2)}`); - return selectedOptions; - } - /** - * Check if the value is higher than the limit, it not returns the limit - * @param value - value to be checked - * @param limit - limit - * @returns - */ - private isHigherThan(limit: number, value?: number): number { - return value && value > limit ? value : limit; - } - /** - * Check if the value is lower than the limit, it not returns the limit - * @param value - value to be checked - * @param limit - limit - * @returns - */ - private isLowerThan(limit: number, value?: number): number { - return value && value < limit ? value : limit; - } - /** - * Forward the command to the downstream producer - * @param job - job to be processed - */ - private readonly onUpstreamCommandHandler = (job: CommandJobHandler): void => { - // Stryker disable all - this.logger.debug(`Received command from upstream: ${job.data.request_id}`); - this.logger.silly(`Direction: ${job.data.from} - ${job.data.to}`); - this.logger.silly( - `Command: ${job.data.content.action} - ${JSON.stringify(job.data.content.target, null, 2)}` - ); - // Stryker enable all - let adaptedCommand: Control.CommandMessage; - try { - adaptedCommand = this.adaptCommand(job.data); - } catch (rawError) { - const error = Crash.from(rawError); - // Stryker disable next-line all - this.logger.error(`Error adapting command: ${error.message}`); - job.done(error as Crash); - return; - } - this.producer - .command(adaptedCommand) - .then(responses => { - for (const response of responses) { - // Stryker disable all - this.logger.debug(`Command response: ${response.request_id}`); - this.logger.silly(`Direction: ${response.from} - ${job.data.to}`); - this.logger.silly(`Command: ${response.content.results?.pairs}`); - // Stryker enable all - } - job.done(); - }) - .catch(job.done); - }; - /** - * Adapt the command to be forwarded - * @param command - command to be processed - * @returns - */ - private adaptCommand(command: Control.CommandMessage): Control.CommandMessage { - const adaptedCommand = cloneDeep(command); - adaptedCommand.from = this.options.id; - adaptedCommand.to = this.getForwardAddresses(command); - adaptedCommand.content.args = this.getArgs(command); - return adaptedCommand; - } - /** - * Get the update args for command execution - * @param command - command to be processed - * @returns - */ - private getArgs(command: Control.CommandMessage): Control.Arguments { - const actualDelay = Accessors.getDelayFromCommandMessage(command); - if (actualDelay - (this.options.delay || MIN_GATEWAY_DELAY) > 0) { - return { - start_time: command.content.args?.start_time, - stop_time: undefined, - duration: actualDelay - (this.options.delay || MIN_GATEWAY_DELAY), - response_requested: command.content.args?.response_requested, - }; - } else { - const error = new Crash( - `No enough time to perform the forwarding of the command`, - this.componentId, - { info: { command, subject: 'OpenC2 Gateway' } } - ); - this.onErrorHandler(error); - throw error; - } - } - /** - * Get the addresses to forward the command - * @param command - command to be forwarded - * @returns - */ - private getForwardAddresses(command: Control.CommandMessage): string[] { - const action = Accessors.getActionFromCommand(command.content); - const target = Accessors.getTargetFromCommand(command.content); - const profile = target.split(':')[0]; - const destinations: string[] = this.producer.consumerMap.getConsumersWithPair(action, target); - const actuator = Accessors.getActuatorAssetId(command.content, profile); - - if (command.to.includes('*')) { - return ['*']; - } else if (actuator) { - return [actuator]; - } else if (destinations.length > 0) { - return destinations; - } else { - const error = new Crash(`No valid destination found for this command`, this.componentId, { - info: { command, subject: 'OpenC2 Gateway' }, - }); - this.onErrorHandler(error); - throw error; - } - } - /** Update the features of the upstream consumer based in the upstream consumer map */ - private readonly updateConsumerOptions = () => { - const { pairs, profiles } = this.producer.consumerMap.getGroupedFeatures(); - this.consumer.pairs = pairs; - this.consumer.profiles = profiles; - }; - /** - * Manage the error in the producer interface - * @param error - error to be processed - */ - protected onErrorHandler(error: unknown): void { - const crash = Crash.from(error); - this.logger.crash(crash); - if (this.listenerCount('error') > 0) { - this.emit('error', crash); - } - } - /** - * Manage the status change in the producer interface - * @param status - status to be processed - */ - private onStatusHandler(status: Health.Status): void { - if (this.listenerCount('status') > 0) { - this.emit('status', status); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { overallStatus } from '@mdf.js/core/dist/Health'; +import { Crash, Links } from '@mdf.js/crash'; +import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; +import EventEmitter from 'events'; +import express from 'express'; +import { cloneDeep } from 'lodash'; +import { v4 } from 'uuid'; +import { Router } from '../../Router'; +import { Accessors } from '../../helpers'; +import { HealthWrapper, Registry } from '../../modules'; +import { + CommandJobHandler, + ConsumerAdapter, + Control, + GatewayOptions, + ProducerAdapter, +} from '../../types'; +import { Consumer } from '../Consumer'; +import { Producer } from '../Producer'; +import { ConsumerMap } from '../Producer/ConsumerMap'; + +const MIN_LOOKUP_INTERVAL = 10000; +const AGING_INTERVAL_FACTOR = 3; +const MAX_AGED_FACTOR = 3; +const MIN_GATEWAY_DELAY = 1000; + +interface GatewayTimers { + lookupInterval: number; + lookupTimeout: number; + agingInterval: number; + maxAge: number; + delay: number; +} +export declare interface Gateway { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + addListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + once(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + off(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + removeListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `error` event + */ + removeAllListeners(event?: 'error'): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + addListener(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + once(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + off(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + removeListener(event: 'status', listener: (status: Health.Status) => void): this; +} + +export class Gateway extends EventEmitter implements Layer.App.Service { + /** Component identification */ + public readonly componentId: string = v4(); + /** Component commands and message register */ + protected readonly register: Registry; + /** Health wrapper instance */ + private readonly health: HealthWrapper; + /** Logger instance */ + private readonly logger: LoggerInstance; + /** Component started flag */ + private started: boolean; + /** Upstream Consumer */ + private readonly consumer: Consumer; + /** Downstream Producer */ + private readonly producer: Producer; + /** Registry router */ + private readonly _router: Router; + /** + * Regular OpenC2 gateway implementation. + * @param upstream - upstream consumer adapter interface + * @param downstream - downstream producer adapter interface + * @param options - configuration options + */ + constructor( + upstream: ConsumerAdapter, + downstream: ProducerAdapter, + private readonly options: GatewayOptions + ) { + super(); + this.logger = SetContext( + this.options.logger ?? new DebugLogger(`mdf:oc2:gateway:${this.name}`), + this.constructor.name, + this.componentId + ); + this.register = + this.options.registry ?? + new Registry(this.options.id, this.options.maxInactivityTime, this.options.registerLimit); + this._router = new Router(this.register); + this.consumer = new Consumer(upstream, { ...this.options, registry: this.register }); + this.producer = new Producer(downstream, { + ...this.options, + ...(this.options.bypassLookupIntervalChecks ? {} : this.checkLookupTimes(this.options)), + registry: this.register, + }); + this.health = new HealthWrapper(this.options.id, [this.consumer, this.producer]); + this.consumer.on('command', this.onUpstreamCommandHandler); + this.producer.consumerMap.on('updated', this.updateConsumerOptions); + this.started = false; + // Stryker disable next-line all + this.logger.debug(`OpenC2 Gateway created - [${options.id}]`); + } + /** Component name */ + public get name(): string { + return this.options.id; + } + /** Component status */ + public get status(): Health.Status { + return overallStatus(this.health.checks); + } + /** + * Return the status of the Consumer in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + return this.health.checks; + } + /** Return an Express router with access to errors registry */ + public get router(): express.Router { + return this._router.router; + } + /** Return links offered by this service */ + public get links(): Links { + return { + openc2: { + jobs: '/openc2/jobs', + pendingJobs: '/openc2/pendingJobs', + messages: '/openc2/messages', + }, + }; + } + /** Connect the OpenC2 underlayer component and perform the startup of the component */ + public start(): Promise { + if (this.started) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.producer + .start() + .then(() => this.consumer.start()) + .then(() => { + this.health.on('error', this.onErrorHandler); + this.health.on('status', this.onStatusHandler); + }) + .then(() => { + this.started = true; + resolve(); + }) + .catch(reject); + }); + } + /** Disconnect the OpenC2 underlayer component and perform the startup of the component */ + public stop(): Promise { + if (!this.started) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.health.off('error', this.onErrorHandler); + this.health.off('status', this.onStatusHandler); + this.consumer + .stop() + .then(() => this.producer.stop()) + .then(() => { + this.started = false; + resolve(); + }) + .catch(reject); + }); + } + /** Close the OpenC2 gateway */ + public close(): Promise { + return this.stop(); + } + /** Consumer Map */ + public get consumerMap(): ConsumerMap { + return this.producer.consumerMap; + } + /** + * Check the lookup time fix them if area wrong + * @param options - configuration options + * @returns + */ + private checkLookupTimes(options: GatewayOptions): GatewayTimers { + const lookupInterval = this.isHigherThan(MIN_LOOKUP_INTERVAL, options.lookupInterval); + const lookupTimeout = this.isLowerThan(lookupInterval / 2, options.lookupTimeout); + const agingInterval = this.isHigherThan( + lookupInterval * AGING_INTERVAL_FACTOR, + options.agingInterval + ); + const maxAge = this.isHigherThan(agingInterval * MAX_AGED_FACTOR, options.maxAge); + const delay = this.isHigherThan(MIN_GATEWAY_DELAY, options.delay); + const selectedOptions = { + lookupInterval, + lookupTimeout, + agingInterval, + maxAge, + delay, + }; + // Stryker disable next-line all + this.logger.debug(`Gateway options ${JSON.stringify(selectedOptions, null, 2)}`); + return selectedOptions; + } + /** + * Check if the value is higher than the limit, it not returns the limit + * @param value - value to be checked + * @param limit - limit + * @returns + */ + private isHigherThan(limit: number, value?: number): number { + return value && value > limit ? value : limit; + } + /** + * Check if the value is lower than the limit, it not returns the limit + * @param value - value to be checked + * @param limit - limit + * @returns + */ + private isLowerThan(limit: number, value?: number): number { + return value && value < limit ? value : limit; + } + /** + * Forward the command to the downstream producer + * @param job - job to be processed + */ + private readonly onUpstreamCommandHandler = (job: CommandJobHandler): void => { + // Stryker disable all + this.logger.debug(`Received command from upstream: ${job.data.request_id}`); + this.logger.silly(`Direction: ${job.data.from} - ${job.data.to}`); + this.logger.silly( + `Command: ${job.data.content.action} - ${JSON.stringify(job.data.content.target, null, 2)}` + ); + // Stryker enable all + let adaptedCommand: Control.CommandMessage; + try { + adaptedCommand = this.adaptCommand(job.data); + } catch (rawError) { + const error = Crash.from(rawError); + // Stryker disable next-line all + this.logger.error(`Error adapting command: ${error.message}`); + job.done(error as Crash); + return; + } + this.producer + .command(adaptedCommand) + .then(responses => { + for (const response of responses) { + // Stryker disable all + this.logger.debug(`Command response: ${response.request_id}`); + this.logger.silly(`Direction: ${response.from} - ${job.data.to}`); + this.logger.silly(`Command: ${response.content.results?.pairs}`); + // Stryker enable all + } + job.done(); + }) + .catch(job.done); + }; + /** + * Adapt the command to be forwarded + * @param command - command to be processed + * @returns + */ + private adaptCommand(command: Control.CommandMessage): Control.CommandMessage { + const adaptedCommand = cloneDeep(command); + adaptedCommand.from = this.options.id; + adaptedCommand.to = this.getForwardAddresses(command); + adaptedCommand.content.args = this.getArgs(command); + return adaptedCommand; + } + /** + * Get the update args for command execution + * @param command - command to be processed + * @returns + */ + private getArgs(command: Control.CommandMessage): Control.Arguments { + const actualDelay = Accessors.getDelayFromCommandMessage(command); + if (actualDelay - (this.options.delay ?? MIN_GATEWAY_DELAY) > 0) { + return { + start_time: command.content.args?.start_time, + stop_time: undefined, + duration: actualDelay - (this.options.delay ?? MIN_GATEWAY_DELAY), + response_requested: command.content.args?.response_requested, + }; + } else { + const error = new Crash( + `No enough time to perform the forwarding of the command`, + this.componentId, + { info: { command, subject: 'OpenC2 Gateway' } } + ); + this.onErrorHandler(error); + throw error; + } + } + /** + * Get the addresses to forward the command + * @param command - command to be forwarded + * @returns + */ + private getForwardAddresses(command: Control.CommandMessage): string[] { + const action = Accessors.getActionFromCommand(command.content); + const target = Accessors.getTargetFromCommand(command.content); + const profile = target.split(':')[0]; + const destinations: string[] = this.producer.consumerMap.getConsumersWithPair(action, target); + const actuator = Accessors.getActuatorAssetId(command.content, profile); + + if (command.to.includes('*')) { + return ['*']; + } else if (actuator) { + return [actuator]; + } else if (destinations.length > 0) { + return destinations; + } else { + const error = new Crash(`No valid destination found for this command`, this.componentId, { + info: { command, subject: 'OpenC2 Gateway' }, + }); + this.onErrorHandler(error); + throw error; + } + } + /** Update the features of the upstream consumer based in the upstream consumer map */ + private readonly updateConsumerOptions = () => { + const { pairs, profiles } = this.producer.consumerMap.getGroupedFeatures(); + this.consumer.pairs = pairs; + this.consumer.profiles = profiles; + }; + /** + * Manage the error in the producer interface + * @param error - error to be processed + */ + protected onErrorHandler(error: unknown): void { + const crash = Crash.from(error); + this.logger.crash(crash); + if (this.listenerCount('error') > 0) { + this.emit('error', crash); + } + } + /** + * Manage the status change in the producer interface + * @param status - status to be processed + */ + private onStatusHandler(status: Health.Status): void { + if (this.listenerCount('status') > 0) { + this.emit('status', status); + } + } +} + diff --git a/packages/api/openc2-core/src/components/Producer/ConsumerMap.ts b/packages/api/openc2-core/src/components/Producer/ConsumerMap.ts index 9eb4255e..29e04ae9 100644 --- a/packages/api/openc2-core/src/components/Producer/ConsumerMap.ts +++ b/packages/api/openc2-core/src/components/Producer/ConsumerMap.ts @@ -1,204 +1,204 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import EventEmitter from 'events'; -import { get, isEqual, uniq } from 'lodash'; -import { v4 } from 'uuid'; -import { Accessors } from '../../helpers'; -import { Control } from '../../types'; - -export declare interface ConsumerMap { - /** Emitted when a producer's operation has some problem */ - on(event: 'error', listener: (error: Crash | Error) => void): this; - /** Emitted on every state change */ - on(event: 'status', listener: (status: Health.Status) => void): this; - /** Emitted when new nodes are included included map */ - on(event: 'new', listener: (nodes: string[]) => void): this; - /** Emitted when some nodes has been aged */ - on(event: 'aged', listener: (nodes: string[]) => void): this; - /** Emitted when nodes are updated */ - on(event: 'update', listener: (nodes: string[]) => void): this; - /** Emitted when the consumer has been updated */ - on(event: 'updated', listener: (nodes: string[]) => void): this; -} - -export class ConsumerMap extends EventEmitter implements Layer.App.Resource { - /** Flag to indicate that an unhealthy status has been emitted recently */ - private lastStatusEmitted?: Health.Status; - /** Component identification */ - public readonly componentId = v4(); - /** Consumer Map */ - private readonly map: Map>; - /** Aging timer */ - private agingTimer?: NodeJS.Timeout; - /** - * Create a new instance of the consumer map - * @param name - name of the producer - * @param agingInterval - agingInterval to update the consumer map - * @param maxAge - Max allowed age in in milliseconds for a table entry - */ - constructor( - public readonly name: string, - private readonly agingInterval: number, - private readonly maxAge: number - ) { - super(); - this.map = new Map(); - } - /** Get the actual nodes */ - public get nodes(): Control.Response[] { - return Array.from(this.map.values()) - .map(node => node.observedValue) - .filter(node => node !== undefined) as Control.Response[]; - } - /** - * Returns the grouped features of all the consumer in the map - * @returns - */ - public getGroupedFeatures(): { pairs: Control.ActionTargetPairs; profiles: string[] } { - const nodes = this.nodes; - const pairs: Control.ActionTargetPairs = {}; - const arrayOfPairs = nodes.map(node => node.results?.pairs || {}); - for (const nodePairs of arrayOfPairs) { - for (const [action, targets] of Object.entries(nodePairs)) { - if (pairs[action as Control.Action]) { - pairs[action as Control.Action] = uniq( - (pairs[action as Control.Action] as string[]).concat(targets) - ); - } else { - pairs[action as Control.Action] = targets; - } - } - } - const profiles = uniq(nodes.map(node => node.results?.profiles ?? []).flat()); - return { pairs, profiles }; - } - /** - * Return the consumers identifiers that has the indicated action/target pair - * @param action - action to search - * @param target - target requested - */ - public getConsumersWithPair(action: string, target: string): string[] { - const consumers: string[] = []; - for (const [consumerId, consumer] of this.map.entries()) { - const targets = get(consumer, `observedValue.results.pairs.${action}`, []) as string[]; - if (targets.includes(target)) { - consumers.push(consumerId); - } - } - return consumers; - } - /** - * Perform the update of the health map - * @param responses - responses to update the consumer map - */ - public update(responses: Control.ResponseMessage[]): void { - const emitAsNewEntry: string[] = []; - const emitAsChanged: string[] = []; - for (const response of responses) { - const actualRegistry = this.map.get(response.from); - if (!actualRegistry) { - emitAsNewEntry.push(response.from); - } else if (!isEqual(actualRegistry.observedValue, response.content)) { - emitAsChanged.push(response.from); - } - this.updateEntry(response); - } - if (emitAsNewEntry.length > 0) { - this.emit('new', emitAsNewEntry); - } - if (emitAsChanged.length > 0) { - this.emit('update', emitAsChanged); - } - if (emitAsChanged.length > 0 || emitAsNewEntry.length > 0) { - this.emit('updated', emitAsChanged.concat(emitAsNewEntry)); - } - this.emitStatus(); - if (!this.agingTimer && this.map.size > 0) { - this.agingTimer = setInterval(this.aging, this.agingInterval); - } - } - /** - * Get one node from the map - * @param consumerId - consumer id to get the status - * @returns - */ - public getNode(consumerId: string): Health.Check | undefined { - return this.map.get(consumerId); - } - /** Return the state of all the underlying consumers */ - public get checks(): Health.Checks { - return { [`${this.name}:consumers`]: Array.from(this.map.values()) }; - } - /** Perform the aging of the consumer map */ - private readonly aging = (): void => { - const emitAsAged: string[] = []; - const checkTime = new Date().getTime(); - for (const [consumerId, consumer] of this.map.entries()) { - const age = checkTime - new Date(consumer.time || 0).getTime(); - if (age > this.maxAge) { - emitAsAged.push(consumerId); - this.map.delete(consumerId); - } - } - if (emitAsAged.length > 0) { - this.emit('aged', emitAsAged); - this.emit('updated', emitAsAged); - } - if (this.map.size === 0) { - clearInterval(this.agingTimer); - this.agingTimer = undefined; - } - }; - /** - * Update the consumer map with a new response - * @param response - response to update the consumer map - */ - private updateEntry(response: Control.ResponseMessage): void { - this.map.set(response.from, { - componentId: response.from, - componentType: 'OpenC2 Consumer', - time: new Date(response.created).toISOString(), - status: Accessors.getStatusFromResponseMessage(response), - observedValue: response.content, - observedUnit: 'features', - }); - } - /** Overall component status */ - public get status(): Health.Status { - return Health.overallStatus(this.checks); - } - /** Emit the status if it's different from the last emitted status */ - private emitStatus(): void { - if (this.lastStatusEmitted !== this.status) { - this.lastStatusEmitted = this.status; - this.emit('status', this.status); - } - } - /** Clean the nodes map */ - public clear(): void { - this.map.clear(); - if (this.agingTimer) { - clearInterval(this.agingTimer); - this.agingTimer = undefined; - } - } - /** Fake start method used to implement the Resource interface */ - public async start(): Promise { - return Promise.resolve(); - } - /** Fake stop method used to implement the Resource interface */ - public async stop(): Promise { - return Promise.resolve(); - } - /** Fake close method used to implement the Resource interface */ - public async close(): Promise { - return Promise.resolve(); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import EventEmitter from 'events'; +import { get, isEqual, uniq } from 'lodash'; +import { v4 } from 'uuid'; +import { Accessors } from '../../helpers'; +import { Control } from '../../types'; + +export declare interface ConsumerMap { + /** Emitted when a producer's operation has some problem */ + on(event: 'error', listener: (error: Crash | Error) => void): this; + /** Emitted on every state change */ + on(event: 'status', listener: (status: Health.Status) => void): this; + /** Emitted when new nodes are included included map */ + on(event: 'new', listener: (nodes: string[]) => void): this; + /** Emitted when some nodes has been aged */ + on(event: 'aged', listener: (nodes: string[]) => void): this; + /** Emitted when nodes are updated */ + on(event: 'update', listener: (nodes: string[]) => void): this; + /** Emitted when the consumer has been updated */ + on(event: 'updated', listener: (nodes: string[]) => void): this; +} + +export class ConsumerMap extends EventEmitter implements Layer.App.Resource { + /** Flag to indicate that an unhealthy status has been emitted recently */ + private lastStatusEmitted?: Health.Status; + /** Component identification */ + public readonly componentId = v4(); + /** Consumer Map */ + private readonly map: Map>; + /** Aging timer */ + private agingTimer?: NodeJS.Timeout; + /** + * Create a new instance of the consumer map + * @param name - name of the producer + * @param agingInterval - agingInterval to update the consumer map + * @param maxAge - Max allowed age in in milliseconds for a table entry + */ + constructor( + public readonly name: string, + private readonly agingInterval: number, + private readonly maxAge: number + ) { + super(); + this.map = new Map(); + } + /** Get the actual nodes */ + public get nodes(): Control.Response[] { + return Array.from(this.map.values()) + .map(node => node.observedValue) + .filter(node => node !== undefined); + } + /** + * Returns the grouped features of all the consumer in the map + * @returns + */ + public getGroupedFeatures(): { pairs: Control.ActionTargetPairs; profiles: string[] } { + const nodes = this.nodes; + const pairs: Control.ActionTargetPairs = {}; + const arrayOfPairs = nodes.map(node => node.results?.pairs || {}); + for (const nodePairs of arrayOfPairs) { + for (const [action, targets] of Object.entries(nodePairs)) { + if (pairs[action as Control.Action]) { + pairs[action as Control.Action] = uniq( + (pairs[action as Control.Action] as string[]).concat(targets) + ); + } else { + pairs[action as Control.Action] = targets; + } + } + } + const profiles = uniq(nodes.map(node => node.results?.profiles ?? []).flat()); + return { pairs, profiles }; + } + /** + * Return the consumers identifiers that has the indicated action/target pair + * @param action - action to search + * @param target - target requested + */ + public getConsumersWithPair(action: string, target: string): string[] { + const consumers: string[] = []; + for (const [consumerId, consumer] of this.map.entries()) { + const targets = get(consumer, `observedValue.results.pairs.${action}`, []) as string[]; + if (targets.includes(target)) { + consumers.push(consumerId); + } + } + return consumers; + } + /** + * Perform the update of the health map + * @param responses - responses to update the consumer map + */ + public update(responses: Control.ResponseMessage[]): void { + const emitAsNewEntry: string[] = []; + const emitAsChanged: string[] = []; + for (const response of responses) { + const actualRegistry = this.map.get(response.from); + if (!actualRegistry) { + emitAsNewEntry.push(response.from); + } else if (!isEqual(actualRegistry.observedValue, response.content)) { + emitAsChanged.push(response.from); + } + this.updateEntry(response); + } + if (emitAsNewEntry.length > 0) { + this.emit('new', emitAsNewEntry); + } + if (emitAsChanged.length > 0) { + this.emit('update', emitAsChanged); + } + if (emitAsChanged.length > 0 || emitAsNewEntry.length > 0) { + this.emit('updated', emitAsChanged.concat(emitAsNewEntry)); + } + this.emitStatus(); + if (!this.agingTimer && this.map.size > 0) { + this.agingTimer = setInterval(this.aging, this.agingInterval); + } + } + /** + * Get one node from the map + * @param consumerId - consumer id to get the status + * @returns + */ + public getNode(consumerId: string): Health.Check | undefined { + return this.map.get(consumerId); + } + /** Return the state of all the underlying consumers */ + public get checks(): Health.Checks { + return { [`${this.name}:consumers`]: Array.from(this.map.values()) }; + } + /** Perform the aging of the consumer map */ + private readonly aging = (): void => { + const emitAsAged: string[] = []; + const checkTime = new Date().getTime(); + for (const [consumerId, consumer] of this.map.entries()) { + const age = checkTime - new Date(consumer.time ?? 0).getTime(); + if (age > this.maxAge) { + emitAsAged.push(consumerId); + this.map.delete(consumerId); + } + } + if (emitAsAged.length > 0) { + this.emit('aged', emitAsAged); + this.emit('updated', emitAsAged); + } + if (this.map.size === 0) { + clearInterval(this.agingTimer); + this.agingTimer = undefined; + } + }; + /** + * Update the consumer map with a new response + * @param response - response to update the consumer map + */ + private updateEntry(response: Control.ResponseMessage): void { + this.map.set(response.from, { + componentId: response.from, + componentType: 'OpenC2 Consumer', + time: new Date(response.created).toISOString(), + status: Accessors.getStatusFromResponseMessage(response), + observedValue: response.content, + observedUnit: 'features', + }); + } + /** Overall component status */ + public get status(): Health.Status { + return Health.overallStatus(this.checks); + } + /** Emit the status if it's different from the last emitted status */ + private emitStatus(): void { + if (this.lastStatusEmitted !== this.status) { + this.lastStatusEmitted = this.status; + this.emit('status', this.status); + } + } + /** Clean the nodes map */ + public clear(): void { + this.map.clear(); + if (this.agingTimer) { + clearInterval(this.agingTimer); + this.agingTimer = undefined; + } + } + /** Fake start method used to implement the Resource interface */ + public async start(): Promise { + return Promise.resolve(); + } + /** Fake stop method used to implement the Resource interface */ + public async stop(): Promise { + return Promise.resolve(); + } + /** Fake close method used to implement the Resource interface */ + public async close(): Promise { + return Promise.resolve(); + } +} diff --git a/packages/api/openc2-core/src/components/Producer/Producer.ts b/packages/api/openc2-core/src/components/Producer/Producer.ts index 2f36b459..8a628d99 100644 --- a/packages/api/openc2-core/src/components/Producer/Producer.ts +++ b/packages/api/openc2-core/src/components/Producer/Producer.ts @@ -1,521 +1,520 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { cloneDeep } from 'lodash'; -import { Accessors, Checkers, Helpers } from '../../helpers'; -import { Control, ProducerAdapter, ProducerOptions } from '../../types'; -import { Component } from '../Component'; -import { ConsumerMap } from './ConsumerMap'; -import { AdapterWrapper } from './core'; - -const DEFAULT_AGING_CHECK_INTERVAL = 1000 * 60; // 1 minute -const DEFAULT_MAX_AGE = DEFAULT_AGING_CHECK_INTERVAL * 3; // 3 minutes - -export declare interface Producer { - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - on(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - addListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Add a listener for the `error` event, emitted when the component detects an error. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - once(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - off(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes the specified listener from the listener array for the `error` event. - * @param event - `error` event - * @param listener - Error event listener - * @event - */ - removeListener(event: 'error', listener: (error: Crash | Error) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `error` event - */ - removeAllListeners(event?: 'error'): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - on(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - addListener(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Add a listener for the `status` event, emitted when the component changes its status. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - once(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - off(event: 'status', listener: (status: Health.Status) => void): this; - /** - * Removes the specified listener from the listener array for the `status` event. - * @param event - `status` event - * @param listener - Status event listener - * @event - */ - removeListener(event: 'status', listener: (status: Health.Status) => void): this; -} - -export class Producer extends Component { - /** Lookup timer */ - private lookupTimer?: NodeJS.Timeout; - /** Consumer Map*/ - public readonly consumerMap: ConsumerMap; - /** - * Regular OpenC2 producer implementation. - * @param adapter - transport adapter - * @param options - configuration options - */ - constructor(adapter: ProducerAdapter, options: ProducerOptions) { - super(new AdapterWrapper(adapter, options.retryOptions), options); - this.consumerMap = new ConsumerMap( - this.name, - this.options.agingInterval || DEFAULT_AGING_CHECK_INTERVAL, - this.options.maxAge || DEFAULT_MAX_AGE - ); - this._router.on('command', this.onCommandHandler); - this.health.add(this.consumerMap); - // Stryker disable next-line all - this.logger.debug(`OpenC2 Producer created - [${options.id}]`); - } - /** Initialize the OpenC2 component */ - protected startup(): Promise { - if (!this.lookupTimer && this.options.lookupInterval && this.options.lookupTimeout) { - this.lookupTimer = setInterval(this.lookup, this.options.lookupInterval); - } - return Promise.resolve(); - } - /** Shutdown the OpenC2 component */ - protected shutdown(): Promise { - if (this.lookupTimer) { - clearInterval(this.lookupTimer); - this.lookupTimer = undefined; - } - return Promise.resolve(); - } - /** - * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command - * will be broadcasted. If an `actuator` is indicated in the command the command will not be - * broadcasted even if it include the '*' symbol. - * @param command - Command to be issued - * @returns - */ - public async command(command: Control.CommandMessage): Promise; - /** - * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command - * will be broadcasted. If an `actuator` is indicated in the command the command will not be - * broadcasted even if it include the '*' symbol. - * @param to - Consumer objetive of this command - * @param content - Command to be issued - * @param id - producer identification - * @returns - */ - public async command(to: string[], content: Control.Command): Promise; - /** - * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command - * will be broadcasted. - * @param to - Consumer objetive of this command - * @param action - command action - * @param target - command target - * @returns - */ - public async command( - to: string[], - action: Control.Action, - target: Control.Target - ): Promise; - public async command( - to: string[] | Control.CommandMessage, - content?: Control.Command | Control.Action, - target?: Control.Target - ): Promise { - try { - const command = this.getCommand(to, content, target); - // Stryker disable all - this.logger.debug(`Request for command: ${command.request_id}`); - this.logger.silly(`Direction: ${command.from} - ${command.to}`); - this.logger.silly( - `Command: ${command.content.action} - ${JSON.stringify(command.content.target, null, 2)}` - ); - // Stryker enable all - if (command.content.args?.response_requested !== Control.ResponseType.None) { - return this.waitForResponse(command); - } else { - // Stryker disable next-line all - this.logger.debug(`Response not requested for command: ${command.request_id}`); - await this.adapter.publish(command); - return []; - } - } catch (error) { - this.onErrorHandler(error); - throw error; - } - } - /** - * Process incoming command message from the router - * @param incomingMessage - incoming message - * @param done - callback to return the response - */ - private readonly onCommandHandler = ( - incomingMessage: Control.CommandMessage, - done: (error?: Crash, response?: Control.ResponseMessage[]) => void - ) => { - this.command(incomingMessage) - .then(result => done(undefined, result)) - .catch(error => done(error, undefined)); - }; - /** Perform the lookup of OpenC2 consumers */ - private readonly lookup: () => void = () => { - const command = Helpers.queryFeatures(this.options.lookupTimeout || 30000); - // Stryker disable next-line all - this.logger.debug(`New lookup command will be emitted`); - this.command(['*'], command) - .then(responses => { - this.consumerMap.update(responses); - }) - .catch(rawError => { - const error = Crash.from(rawError); - this.onErrorHandler( - new Crash(`Error performing a new lookup: ${error.message}`, this.componentId, { - cause: rawError, - }) - ); - }); - }; - /** - * Get and validate the command message to be published - * @param destinationsOrCommand - destination of command defined in content or command itself - * @param contentOrAction - Command to be issued - * @param id - producer identification - * @returns - */ - private getCommand( - destinationsOrCommand: string[] | Control.CommandMessage, - contentOrAction?: Control.Command | Control.Action, - target?: Control.Target - ): Control.CommandMessage { - let message: Control.CommandMessage; - if ( - !Array.isArray(destinationsOrCommand) && - contentOrAction === undefined && - destinationsOrCommand - ) { - message = destinationsOrCommand; - } else if ( - Array.isArray(destinationsOrCommand) && - typeof contentOrAction === 'object' && - target === undefined - ) { - message = Helpers.createCommand(destinationsOrCommand, contentOrAction, this.options.id); - } else if ( - Array.isArray(destinationsOrCommand) && - typeof contentOrAction === 'string' && - typeof target === 'object' - ) { - message = Helpers.createCommandByAction( - destinationsOrCommand, - contentOrAction, - target, - this.options.id - ); - } else { - throw new Crash(`Invalid type of parameters in command creation`, this.componentId); - } - return Checkers.isValidCommandSync(message, this.adapter.componentId); - } - /** - * Wait for the response to an issued command - * @param command - issued command - */ - private waitForResponse(command: Control.CommandMessage): Promise { - if ( - command.to.includes('*') || - command.to.length > 1 || - Accessors.getActuatorsFromCommandMessage(command).length > 0 - ) { - return this.waitForBroadCastResponses(command); - } else { - return Promise.all( - command.to.map(consumer => { - const singleConsumerCommand = cloneDeep(command); - singleConsumerCommand.to = [consumer]; - return this.waitForConsumerResponse(singleConsumerCommand); - }) - ); - } - } - /** - * Wait for the response of several consumers during the defined duration in the command - * @param command - issued command - * @returns - */ - private waitForBroadCastResponses( - command: Control.CommandMessage - ): Promise { - return new Promise((resolve, reject) => { - const requestId = command.request_id; - const responses: Control.ResponseMessage[] = []; - const timeout = Accessors.getDelayFromCommandMessage(command); - // Stryker disable next-line all - this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`); - // ******************************************************************************************* - // #region Incoming response handler - const broadcastResponseHandler = this.getBroadcastResponseHandler(command, responses); - // #endregion - // ******************************************************************************************* - // #region On timeout event handler - const onTimeout: () => void = () => { - // Stryker disable next-line all - this.logger.debug( - `Timeout complete for command: ${requestId}, all the responses will be resolved` - ); - this.adapter.off(requestId, broadcastResponseHandler); - resolve(responses); - }; - // #endregion - // ******************************************************************************************* - // #region On directly responses to publish method - const onDirectResponse = ( - result: void | Control.ResponseMessage | Control.ResponseMessage[] - ) => { - if (result) { - const directResponses = Array.isArray(result) ? result : [result]; - for (const response of directResponses) { - broadcastResponseHandler(response); - } - this.adapter.off(requestId, broadcastResponseHandler); - clearTimeout(timeoutTimer); - resolve(directResponses); - } - }; - // #endregion - // ******************************************************************************************* - // #region On error on publish method - const onPublishError = (rawError: unknown) => { - clearTimeout(timeoutTimer); - this.adapter.off(requestId, broadcastResponseHandler); - const error = Crash.from(rawError); - const publishingError = new Crash( - `Error publishing command to control channel: ${error.message}`, - this.componentId, - { cause: error } - ); - this.onErrorHandler(publishingError); - reject(publishingError); - }; - // #endregion - // ******************************************************************************************* - // #region Subscription to responses - this.adapter.on(requestId, broadcastResponseHandler); - const timeoutTimer = setTimeout(onTimeout, timeout); - this.adapter.publish(command).then(onDirectResponse).catch(onPublishError); - }); - } - /** - * Wait for the response of several consumers during the defined duration in the command - * @param command - issued command - * @param responses - responses array - */ - private getBroadcastResponseHandler( - command: Control.CommandMessage, - responses: Control.ResponseMessage[] - ): (incomingMessage: Control.Message) => void { - const requestId = command.request_id; - return (incomingMessage: Control.Message) => { - try { - const message = Checkers.isValidResponseSync(incomingMessage, this.componentId); - // Stryker disable next-line all - this.logger.debug(`New message from ${message.from} - ${message.request_id}`); - if (!Checkers.isResponseToInstance(message, command.from, requestId)) { - // Stryker disable next-line all - this.logger.debug(`${message.request_id} is not a response for this instance`); - return; - } else if (message.request_id === requestId) { - this.register.push(message); - if (message.status >= 200) { - // Stryker disable next-line all - this.logger.debug(`Response from: [${message.from}] received`); - responses.push(message); - } else { - // Stryker disable next-line all - this.logger.debug(`ACK from: [${message.from}] received`); - if (command.content.args?.response_requested === Control.ResponseType.ACK) { - responses.push(message); - } - } - } - } catch (rawError) { - this.onProcessingMessageError(rawError); - } - }; - } - /** - * Wait for the response to an issued command from one consumer - * @param command - issued command - */ - private waitForConsumerResponse( - command: Control.CommandMessage - ): Promise { - return new Promise((resolve, reject) => { - const requestId = command.request_id; - const timeout = Accessors.getDelayFromCommandMessage(command); - // Stryker disable next-line all - this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`); - // ******************************************************************************************* - // #region Incoming response handler - const consumerResponseHandler = (incomingMessage: Control.Message): void => { - try { - const message = Checkers.isValidResponseSync(incomingMessage, this.componentId); - // Stryker disable next-line all - this.logger.debug(`New message from ${message.from} - ${message.request_id}`); - if (!Checkers.isResponseToInstance(message, command.from, requestId)) { - // Stryker disable next-line all - this.logger.debug(`${message.request_id} is not a response for this instance`); - } else if (message.request_id === requestId) { - try { - const result = this.handlerMessage(message, command); - if (result) { - clearTimeout(timeoutTimer); - this.adapter.off(requestId, consumerResponseHandler); - resolve(result); - } - } catch (rawError) { - clearTimeout(timeoutTimer); - this.adapter.off(requestId, consumerResponseHandler); - reject(rawError); - } - } - } catch (rawError) { - this.onProcessingMessageError(rawError); - } - }; - // #endregion - // ******************************************************************************************* - // #region On timeout event handler - const onTimeout: () => void = () => { - const timeOutError = new Crash( - `Response timeout for the command ${requestId} [${command.content.action}]`, - requestId - ); - this.logger.debug(timeOutError.message); - this.adapter.off(requestId, consumerResponseHandler); - reject(timeOutError); - }; - // #endregion - // ******************************************************************************************* - // #region On directly responses to publish method - const onDirectResponse = ( - result: void | Control.ResponseMessage | Control.ResponseMessage[] - ) => { - if (Array.isArray(result)) { - clearTimeout(timeoutTimer); - this.adapter.off(requestId, consumerResponseHandler); - reject( - new Crash( - `Command to a single destination was resolved with multiple responses: ${result.length}`, - requestId, - { info: result } - ) - ); - } else if (result) { - consumerResponseHandler(result); - } - }; - // #endregion - // ******************************************************************************************* - // #region On error on publish method - const onPublishError = (rawError: unknown) => { - clearTimeout(timeoutTimer); - this.adapter.off(requestId, consumerResponseHandler); - const error = Crash.from(rawError); - const publishingError = new Crash( - `Error publishing command to control channel: ${error.message}`, - this.componentId, - { cause: error } - ); - this.onErrorHandler(publishingError); - reject(publishingError); - }; - // #endregion - this.adapter.on(requestId, consumerResponseHandler); - const timeoutTimer = setTimeout(onTimeout, timeout); - this.adapter.publish(command).then(onDirectResponse).catch(onPublishError); - }); - } - private handlerMessage( - message: Control.ResponseMessage, - command: Control.CommandMessage - ): Control.ResponseMessage | undefined { - this.register.push(message); - if (message.status >= 200 && message.status < 300) { - // Stryker disable next-line all - this.logger.debug(`Response from: [${message.from}] received - Fulfilled`); - return message; - } else if (message.status >= 300) { - // Stryker disable next-line all - this.logger.warn(`Response from: [${message.from}] received - Not fulfilled`); - throw new Crash(`Command was not fulfilled: [status ${message.status}]`, message.request_id); - } else { - // Stryker disable next-line all - this.logger.debug(`ACK from: [${message.from}] received`); - if (command.content.args?.response_requested === Control.ResponseType.ACK) { - return message; - } - return undefined; - } - } - /** - * Handle errors processing incoming messages - * @param rawError - raw error - */ - private readonly onProcessingMessageError = (rawError: unknown) => { - const error = Crash.from(rawError); - const processingError = new Crash( - `Error processing incoming response message from control chanel: ${error.message}`, - this.componentId, - { cause: error } - ); - this.onErrorHandler(processingError); - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { cloneDeep } from 'lodash'; +import { Accessors, Checkers, Helpers } from '../../helpers'; +import { Control, ProducerAdapter, ProducerOptions } from '../../types'; +import { Component } from '../Component'; +import { ConsumerMap } from './ConsumerMap'; +import { AdapterWrapper } from './core'; + +const DEFAULT_AGING_CHECK_INTERVAL = 1000 * 60; // 1 minute +const DEFAULT_MAX_AGE = DEFAULT_AGING_CHECK_INTERVAL * 3; // 3 minutes + +export declare interface Producer { + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + on(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + addListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Add a listener for the `error` event, emitted when the component detects an error. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + once(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + off(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes the specified listener from the listener array for the `error` event. + * @param event - `error` event + * @param listener - Error event listener + * @event + */ + removeListener(event: 'error', listener: (error: Crash | Error) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `error` event + */ + removeAllListeners(event?: 'error'): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + on(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + addListener(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Add a listener for the `status` event, emitted when the component changes its status. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + once(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + off(event: 'status', listener: (status: Health.Status) => void): this; + /** + * Removes the specified listener from the listener array for the `status` event. + * @param event - `status` event + * @param listener - Status event listener + * @event + */ + removeListener(event: 'status', listener: (status: Health.Status) => void): this; +} + +export class Producer extends Component { + /** Lookup timer */ + private lookupTimer?: NodeJS.Timeout; + /** Consumer Map*/ + public readonly consumerMap: ConsumerMap; + /** + * Regular OpenC2 producer implementation. + * @param adapter - transport adapter + * @param options - configuration options + */ + constructor(adapter: ProducerAdapter, options: ProducerOptions) { + super(new AdapterWrapper(adapter, options.retryOptions), options); + this.consumerMap = new ConsumerMap( + this.name, + this.options.agingInterval ?? DEFAULT_AGING_CHECK_INTERVAL, + this.options.maxAge ?? DEFAULT_MAX_AGE + ); + this._router.on('command', this.onCommandHandler); + this.health.add(this.consumerMap); + // Stryker disable next-line all + this.logger.debug(`OpenC2 Producer created - [${options.id}]`); + } + /** Initialize the OpenC2 component */ + protected startup(): Promise { + if (!this.lookupTimer && this.options.lookupInterval && this.options.lookupTimeout) { + this.lookupTimer = setInterval(this.lookup, this.options.lookupInterval); + } + return Promise.resolve(); + } + /** Shutdown the OpenC2 component */ + protected shutdown(): Promise { + if (this.lookupTimer) { + clearInterval(this.lookupTimer); + this.lookupTimer = undefined; + } + return Promise.resolve(); + } + /** + * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command + * will be broadcasted. If an `actuator` is indicated in the command the command will not be + * broadcasted even if it include the '*' symbol. + * @param command - Command to be issued + * @returns + */ + public async command(command: Control.CommandMessage): Promise; + /** + * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command + * will be broadcasted. If an `actuator` is indicated in the command the command will not be + * broadcasted even if it include the '*' symbol. + * @param to - Consumer objetive of this command + * @param content - Command to be issued + * @returns + */ + public async command(to: string[], content: Control.Command): Promise; + /** + * Issue a new command to the requested consumers. If '*' is indicated as a consumer, the command + * will be broadcasted. + * @param to - Consumer objetive of this command + * @param action - command action + * @param target - command target + * @returns + */ + public async command( + to: string[], + action: Control.Action, + target: Control.Target + ): Promise; + public async command( + to: string[] | Control.CommandMessage, + content?: Control.Command | Control.Action, + target?: Control.Target + ): Promise { + try { + const command = this.getCommand(to, content, target); + // Stryker disable all + this.logger.debug(`Request for command: ${command.request_id}`); + this.logger.silly(`Direction: ${command.from} - ${command.to}`); + this.logger.silly( + `Command: ${command.content.action} - ${JSON.stringify(command.content.target, null, 2)}` + ); + // Stryker enable all + if (command.content.args?.response_requested !== Control.ResponseType.None) { + return this.waitForResponse(command); + } else { + // Stryker disable next-line all + this.logger.debug(`Response not requested for command: ${command.request_id}`); + await this.adapter.publish(command); + return []; + } + } catch (error) { + this.onErrorHandler(error); + throw error; + } + } + /** + * Process incoming command message from the router + * @param incomingMessage - incoming message + * @param done - callback to return the response + */ + private readonly onCommandHandler = ( + incomingMessage: Control.CommandMessage, + done: (error?: Crash, response?: Control.ResponseMessage[]) => void + ) => { + this.command(incomingMessage) + .then(result => done(undefined, result)) + .catch(error => done(error, undefined)); + }; + /** Perform the lookup of OpenC2 consumers */ + private readonly lookup: () => void = () => { + const command = Helpers.queryFeatures(this.options.lookupTimeout ?? 30000); + // Stryker disable next-line all + this.logger.debug(`New lookup command will be emitted`); + this.command(['*'], command) + .then(responses => { + this.consumerMap.update(responses); + }) + .catch(rawError => { + const error = Crash.from(rawError); + this.onErrorHandler( + new Crash(`Error performing a new lookup: ${error.message}`, this.componentId, { + cause: rawError, + }) + ); + }); + }; + /** + * Get and validate the command message to be published + * @param destinationsOrCommand - destination of command defined in content or command itself + * @param contentOrAction - Command to be issued + * @param id - producer identification + * @returns + */ + private getCommand( + destinationsOrCommand: string[] | Control.CommandMessage, + contentOrAction?: Control.Command | Control.Action, + target?: Control.Target + ): Control.CommandMessage { + let message: Control.CommandMessage; + if ( + !Array.isArray(destinationsOrCommand) && + contentOrAction === undefined && + destinationsOrCommand + ) { + message = destinationsOrCommand; + } else if ( + Array.isArray(destinationsOrCommand) && + typeof contentOrAction === 'object' && + target === undefined + ) { + message = Helpers.createCommand(destinationsOrCommand, contentOrAction, this.options.id); + } else if ( + Array.isArray(destinationsOrCommand) && + typeof contentOrAction === 'string' && + typeof target === 'object' + ) { + message = Helpers.createCommandByAction( + destinationsOrCommand, + contentOrAction, + target, + this.options.id + ); + } else { + throw new Crash(`Invalid type of parameters in command creation`, this.componentId); + } + return Checkers.isValidCommandSync(message, this.adapter.componentId); + } + /** + * Wait for the response to an issued command + * @param command - issued command + */ + private waitForResponse(command: Control.CommandMessage): Promise { + if ( + command.to.includes('*') || + command.to.length > 1 || + Accessors.getActuatorsFromCommandMessage(command).length > 0 + ) { + return this.waitForBroadCastResponses(command); + } else { + return Promise.all( + command.to.map(consumer => { + const singleConsumerCommand = cloneDeep(command); + singleConsumerCommand.to = [consumer]; + return this.waitForConsumerResponse(singleConsumerCommand); + }) + ); + } + } + /** + * Wait for the response of several consumers during the defined duration in the command + * @param command - issued command + * @returns + */ + private waitForBroadCastResponses( + command: Control.CommandMessage + ): Promise { + return new Promise((resolve, reject) => { + const requestId = command.request_id; + const responses: Control.ResponseMessage[] = []; + const timeout = Accessors.getDelayFromCommandMessage(command); + // Stryker disable next-line all + this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`); + // ******************************************************************************************* + // #region Incoming response handler + const broadcastResponseHandler = this.getBroadcastResponseHandler(command, responses); + // #endregion + // ******************************************************************************************* + // #region On timeout event handler + const onTimeout: () => void = () => { + // Stryker disable next-line all + this.logger.debug( + `Timeout complete for command: ${requestId}, all the responses will be resolved` + ); + this.adapter.off(requestId, broadcastResponseHandler); + resolve(responses); + }; + // #endregion + // ******************************************************************************************* + // #region On directly responses to publish method + const onDirectResponse = ( + result: void | Control.ResponseMessage | Control.ResponseMessage[] + ) => { + if (result) { + const directResponses = Array.isArray(result) ? result : [result]; + for (const response of directResponses) { + broadcastResponseHandler(response); + } + this.adapter.off(requestId, broadcastResponseHandler); + clearTimeout(timeoutTimer); + resolve(directResponses); + } + }; + // #endregion + // ******************************************************************************************* + // #region On error on publish method + const onPublishError = (rawError: unknown) => { + clearTimeout(timeoutTimer); + this.adapter.off(requestId, broadcastResponseHandler); + const error = Crash.from(rawError); + const publishingError = new Crash( + `Error publishing command to control channel: ${error.message}`, + this.componentId, + { cause: error } + ); + this.onErrorHandler(publishingError); + reject(publishingError); + }; + // #endregion + // ******************************************************************************************* + // #region Subscription to responses + this.adapter.on(requestId, broadcastResponseHandler); + const timeoutTimer = setTimeout(onTimeout, timeout); + this.adapter.publish(command).then(onDirectResponse).catch(onPublishError); + }); + } + /** + * Wait for the response of several consumers during the defined duration in the command + * @param command - issued command + * @param responses - responses array + */ + private getBroadcastResponseHandler( + command: Control.CommandMessage, + responses: Control.ResponseMessage[] + ): (incomingMessage: Control.Message) => void { + const requestId = command.request_id; + return (incomingMessage: Control.Message) => { + try { + const message = Checkers.isValidResponseSync(incomingMessage, this.componentId); + // Stryker disable next-line all + this.logger.debug(`New message from ${message.from} - ${message.request_id}`); + if (!Checkers.isResponseToInstance(message, command.from, requestId)) { + // Stryker disable next-line all + this.logger.debug(`${message.request_id} is not a response for this instance`); + return; + } else if (message.request_id === requestId) { + this.register.push(message); + if (message.status >= 200) { + // Stryker disable next-line all + this.logger.debug(`Response from: [${message.from}] received`); + responses.push(message); + } else { + // Stryker disable next-line all + this.logger.debug(`ACK from: [${message.from}] received`); + if (command.content.args?.response_requested === Control.ResponseType.ACK) { + responses.push(message); + } + } + } + } catch (rawError) { + this.onProcessingMessageError(rawError); + } + }; + } + /** + * Wait for the response to an issued command from one consumer + * @param command - issued command + */ + private waitForConsumerResponse( + command: Control.CommandMessage + ): Promise { + return new Promise((resolve, reject) => { + const requestId = command.request_id; + const timeout = Accessors.getDelayFromCommandMessage(command); + // Stryker disable next-line all + this.logger.debug(`Timeout for responses to command ${requestId}: ${timeout} ms`); + // ******************************************************************************************* + // #region Incoming response handler + const consumerResponseHandler = (incomingMessage: Control.Message): void => { + try { + const message = Checkers.isValidResponseSync(incomingMessage, this.componentId); + // Stryker disable next-line all + this.logger.debug(`New message from ${message.from} - ${message.request_id}`); + if (!Checkers.isResponseToInstance(message, command.from, requestId)) { + // Stryker disable next-line all + this.logger.debug(`${message.request_id} is not a response for this instance`); + } else if (message.request_id === requestId) { + try { + const result = this.handlerMessage(message, command); + if (result) { + clearTimeout(timeoutTimer); + this.adapter.off(requestId, consumerResponseHandler); + resolve(result); + } + } catch (rawError) { + clearTimeout(timeoutTimer); + this.adapter.off(requestId, consumerResponseHandler); + reject(Crash.from(rawError)); + } + } + } catch (rawError) { + this.onProcessingMessageError(rawError); + } + }; + // #endregion + // ******************************************************************************************* + // #region On timeout event handler + const onTimeout: () => void = () => { + const timeOutError = new Crash( + `Response timeout for the command ${requestId} [${command.content.action}]`, + requestId + ); + this.logger.debug(timeOutError.message); + this.adapter.off(requestId, consumerResponseHandler); + reject(timeOutError); + }; + // #endregion + // ******************************************************************************************* + // #region On directly responses to publish method + const onDirectResponse = ( + result: void | Control.ResponseMessage | Control.ResponseMessage[] + ) => { + if (Array.isArray(result)) { + clearTimeout(timeoutTimer); + this.adapter.off(requestId, consumerResponseHandler); + reject( + new Crash( + `Command to a single destination was resolved with multiple responses: ${result.length}`, + requestId, + { info: result } + ) + ); + } else if (result) { + consumerResponseHandler(result); + } + }; + // #endregion + // ******************************************************************************************* + // #region On error on publish method + const onPublishError = (rawError: unknown) => { + clearTimeout(timeoutTimer); + this.adapter.off(requestId, consumerResponseHandler); + const error = Crash.from(rawError); + const publishingError = new Crash( + `Error publishing command to control channel: ${error.message}`, + this.componentId, + { cause: error } + ); + this.onErrorHandler(publishingError); + reject(publishingError); + }; + // #endregion + this.adapter.on(requestId, consumerResponseHandler); + const timeoutTimer = setTimeout(onTimeout, timeout); + this.adapter.publish(command).then(onDirectResponse).catch(onPublishError); + }); + } + private handlerMessage( + message: Control.ResponseMessage, + command: Control.CommandMessage + ): Control.ResponseMessage | undefined { + this.register.push(message); + if (message.status >= 200 && message.status < 300) { + // Stryker disable next-line all + this.logger.debug(`Response from: [${message.from}] received - Fulfilled`); + return message; + } else if (message.status >= 300) { + // Stryker disable next-line all + this.logger.warn(`Response from: [${message.from}] received - Not fulfilled`); + throw new Crash(`Command was not fulfilled: [status ${message.status}]`, message.request_id); + } else { + // Stryker disable next-line all + this.logger.debug(`ACK from: [${message.from}] received`); + if (command.content.args?.response_requested === Control.ResponseType.ACK) { + return message; + } + return undefined; + } + } + /** + * Handle errors processing incoming messages + * @param rawError - raw error + */ + private readonly onProcessingMessageError = (rawError: unknown) => { + const error = Crash.from(rawError); + const processingError = new Crash( + `Error processing incoming response message from control chanel: ${error.message}`, + this.componentId, + { cause: error } + ); + this.onErrorHandler(processingError); + }; +} diff --git a/packages/api/openc2-core/src/components/Producer/index.ts b/packages/api/openc2-core/src/components/Producer/index.ts index 415a3fe5..0916d7aa 100644 --- a/packages/api/openc2-core/src/components/Producer/index.ts +++ b/packages/api/openc2-core/src/components/Producer/index.ts @@ -1,7 +1,10 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -export * from './Producer'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './ConsumerMap'; +export * from './Producer'; + diff --git a/packages/api/openc2-core/src/helpers/Accessors.ts b/packages/api/openc2-core/src/helpers/Accessors.ts index cef3a037..dcc5a286 100644 --- a/packages/api/openc2-core/src/helpers/Accessors.ts +++ b/packages/api/openc2-core/src/helpers/Accessors.ts @@ -1,105 +1,113 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health } from '@mdf.js/core'; -import { get } from 'lodash'; -import { Control } from '../types'; -import { Constants } from './Constants'; - -export class Accessors { - /** - * Return the target of the actual message - * @param command - message to be processed - */ - public static getTargetFromCommandMessage(command: Control.CommandMessage): string { - return Object.keys(command.content.target)[0]; - } - /** - * Return the target of the actual command - * @param command - command to be processed - */ - public static getTargetFromCommand(command: Control.Command): string { - return Object.keys(command.target)[0]; - } - /** - * Return the action of the actual message - * @param command - message to be processed - */ - public static getActionFromCommandMessage(command: Control.CommandMessage): Control.Action { - return command.content.action; - } - /** - * Return the action of the actual command - * @param command - command to be processed - */ - public static getActionFromCommand(command: Control.Command): Control.Action { - return command.action; - } - /** - * Return the actuators in the command message - * @param command - message to be processed - */ - public static getActuatorsFromCommandMessage(command: Control.CommandMessage): string[] { - return this.getActuatorsFromCommand(command.content); - } - /** - * Return the actuators in the command - * @param command - command to be processed - */ - public static getActuatorsFromCommand(command: Control.Command): string[] { - const actuators = get(command, 'actuator', {}); - return Object.keys(actuators); - } - /** - * Return the a property from actuators in the command - * @param command - command to be processed - * @param profile - actuator profile to find - * @param property - property to find - */ - public static getActuatorAssetId(command: Control.Command, profile: string): any { - return get(command, ['actuator', profile, 'asset_id'], undefined); - } - /** - * Return the delay allowed from command message - * @param command - message to be processed - */ - public static getDelayFromCommandMessage(command: Control.CommandMessage): number { - return this.getDelayFromCommand(command.content); - } - /** - * Return the delay allowed from command - * @param command - message to be processed - */ - public static getDelayFromCommand(command: Control.Command): number { - const startTime = get(command, 'args.start_time', undefined); - const stopTime = get(command, 'args.stop_time', undefined); - const duration = get(command, 'args.duration', undefined); - let delay: number; - if (stopTime !== undefined) { - delay = stopTime - Date.now(); - } else if (startTime !== undefined && duration !== undefined) { - delay = startTime + duration - Date.now(); - } else { - delay = duration || Constants.DEFAULT_MAX_RESPONSE_COMMAND_DELAY; - } - return delay > 0 ? delay : Constants.DEFAULT_MAX_RESPONSE_COMMAND_DELAY; - } - /** - * Convert consumer status to Subcomponent status - * @param status - consumer status - * @returns - */ - public static getStatusFromResponseMessage(response: Control.ResponseMessage): Health.Status { - if (response.status >= 500) { - return 'fail'; - } else if (response.status >= 200) { - return 'pass'; - } else { - return 'warn'; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health } from '@mdf.js/core'; +import { get } from 'lodash'; +import { Control } from '../types'; +import { Constants } from './Constants'; + +export class Accessors { + /** + * Return the target of the actual message + * @param command - message to be processed + * @returns target + */ + public static getTargetFromCommandMessage(command: Control.CommandMessage): string { + return Object.keys(command.content.target)[0]; + } + /** + * Return the target of the actual command + * @param command - command to be processed + * @returns target + */ + public static getTargetFromCommand(command: Control.Command): string { + return Object.keys(command.target)[0]; + } + /** + * Return the action of the actual message + * @param command - message to be processed + * @returns action + */ + public static getActionFromCommandMessage(command: Control.CommandMessage): Control.Action { + return command.content.action; + } + /** + * Return the action of the actual command + * @param command - command to be processed + * @returns action + */ + public static getActionFromCommand(command: Control.Command): Control.Action { + return command.action; + } + /** + * Return the actuators in the command message + * @param command - message to be processed + * @returns actuators + */ + public static getActuatorsFromCommandMessage(command: Control.CommandMessage): string[] { + return this.getActuatorsFromCommand(command.content); + } + /** + * Return the actuators in the command + * @param command - command to be processed + * @returns actuators + */ + public static getActuatorsFromCommand(command: Control.Command): string[] { + const actuators = get(command, 'actuator', {}); + return Object.keys(actuators); + } + /** + * Return the a property from actuators in the command + * @param command - command to be processed + * @param profile - actuator profile to find + * @returns property value + */ + public static getActuatorAssetId(command: Control.Command, profile: string): any { + return get(command, ['actuator', profile, 'asset_id'], undefined); + } + /** + * Return the delay allowed from command message + * @param command - message to be processed + * @returns delay in milliseconds + */ + public static getDelayFromCommandMessage(command: Control.CommandMessage): number { + return this.getDelayFromCommand(command.content); + } + /** + * Return the delay allowed from command + * @param command - message to be processed + * @returns delay in milliseconds + */ + public static getDelayFromCommand(command: Control.Command): number { + const startTime = get(command, 'args.start_time', undefined); + const stopTime = get(command, 'args.stop_time', undefined); + const duration = get(command, 'args.duration', undefined); + let delay: number; + if (stopTime !== undefined) { + delay = stopTime - Date.now(); + } else if (startTime !== undefined && duration !== undefined) { + delay = startTime + duration - Date.now(); + } else { + delay = duration ?? Constants.DEFAULT_MAX_RESPONSE_COMMAND_DELAY; + } + return delay > 0 ? delay : Constants.DEFAULT_MAX_RESPONSE_COMMAND_DELAY; + } + /** + * Convert consumer status to Subcomponent status + * @param response - response message to be processed + * @returns Subcomponent status + */ + public static getStatusFromResponseMessage(response: Control.ResponseMessage): Health.Status { + if (response.status >= 500) { + return 'fail'; + } else if (response.status >= 200) { + return 'pass'; + } else { + return 'warn'; + } + } +} diff --git a/packages/api/openc2-core/src/helpers/Checkers.ts b/packages/api/openc2-core/src/helpers/Checkers.ts index 7e261f0a..a94ef7db 100644 --- a/packages/api/openc2-core/src/helpers/Checkers.ts +++ b/packages/api/openc2-core/src/helpers/Checkers.ts @@ -1,224 +1,222 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { get, set } from 'lodash'; -import { Checker } from '../modules'; -import { ConsumerOptions, Control } from '../types'; -import { Accessors } from './Accessors'; -import { Helpers } from './Helpers'; - -const responseRequested = 'content.args.response_requested'; -export class Checkers { - /** - * Check if the message is a valid message. - * @param message - message to be check - * @param uuid - traceability identifier - * @returns Validated message. - * @throws In case of invalid message, throw a validation error. - */ - public static isValidMessageSync(message: Control.Message, uuid: string): Control.Message { - return Checker.attempt('Control.Message', message, uuid); - } - /** - * Check if the message is a valid message. - * @param message - message to be check - * @param uuid - traceability identifier - * @returns Promise with the validated message. - */ - public static isValidMessage(message: Control.Message, uuid: string): Promise { - return Checker.validate('Control.Message', message, uuid); - } - /** - * Check if the command is a valid command message. - * @param command - command to be check - * @param uuid - traceability identifier - * @returns Validated command message. - * @throws In case of invalid command, throw a validation error. - */ - public static isValidCommandSync(command: Control.Message, uuid: string): Control.CommandMessage { - return Checker.attempt('Control.Message.Command', command, uuid); - } - /** - * Check if the command is a valid command message. - * @param command - command to be check - * @param uuid - traceability identifier - * @returns Promise with the validated command message. - */ - public static isValidCommand( - command: Control.Message, - uuid: string - ): Promise { - return Checker.validate('Control.Message.Command', command, uuid); - } - /** - * Check if the response is a valid response message - * @param response - response to be checked - * @param uuid - traceability identifier - * @returns Validated response message. - * @throws In case of invalid response, throw a validation error. - */ - public static isValidResponseSync( - response: Control.Message, - uuid: string - ): Control.ResponseMessage { - return Checker.attempt('Control.Message.Response', response, uuid); - } - /** - * Check if the response is a valid response message - * @param response - response to be checked - * @param uuid - traceability identifier - * @returns Promise with the validated response message. - */ - public static isValidResponse( - response: Control.Message, - uuid: string - ): Promise { - return Checker.validate('Control.Message.Response', response, uuid); - } - /** - * Checks if the command should be response with a default response - * @param command - message to be checked - * @param options - Consumer options - * @returns Default response for the command or undefined if the command has no default response - */ - public static hasDefaultResponse( - command: Control.CommandMessage, - options: ConsumerOptions - ): Control.ResponseMessage | undefined { - if (!this.isOnTime(command)) { - return Helpers.badRequest(command, options.id, 'Command is out of time'); - } else if (this.isQueryFeaturesRequest(command)) { - if (this.isValidQueryFeaturesRequest(command)) { - return Helpers.respondFeatures( - command, - options.id, - options.actionTargetPairs, - options.profiles - ); - } else { - set(command, responseRequested, Control.ResponseType.Complete); - return Helpers.badRequest(command, options.id, 'Invalid Query Features'); - } - } else if (!this.isSupportedAction(command, options.actionTargetPairs)) { - return Helpers.notImplemented(command, options.id, 'Command not supported'); - } else if (this.isAckOnlyRequested(command)) { - return Helpers.processing(command, options.id, undefined, 'Command accepted'); - } else { - return undefined; - } - } - /** - * Check if the message is a command the instance indicated or for all the instances - * @param message - message to be checked - * @param id - instance identification - * @returns true if the message is for this instance or for all the instances - */ - public static isCommandToInstance(message: Control.Message, id: string): boolean { - return ( - message.msg_type === Control.MessageType.Command && - (message.to.includes(id) || message.to.includes('*')) && - message.from !== id - ); - } - /** - * Check if the message is a response for our command - * @param message - message to be checked - * @param from - from field of the original command - * @param requestId - request_id field of the original command - * @returns - */ - public static isResponseToInstance( - message: Control.Message, - from: string, - requestId: string - ): boolean { - return ( - message.msg_type === Control.MessageType.Response && - (message.to.includes(from) || message.to.includes('*')) && - message.request_id === requestId - ); - } - /** - * Check if the command has arguments based on execution time and if we are on time - * @param command - message to be checked - * @returns - */ - public static isOnTime(command: Control.CommandMessage): boolean { - const startTime = get(command, 'content.args.start_time', undefined); - const stopTime = get(command, 'content.args.stop_time', undefined); - const duration = get(command, 'content.args.duration', undefined); - const wrongConfiguration = - startTime !== undefined && stopTime !== undefined && duration !== undefined; - const startTimeHasPassedBasedOnStartTime = - startTime !== undefined && duration !== undefined && startTime + duration < Date.now(); - const startTimeHasPassedBasedOnStopTime = - startTime === undefined && - stopTime !== undefined && - duration !== undefined && - stopTime < Date.now(); - return ( - !wrongConfiguration && - !startTimeHasPassedBasedOnStartTime && - !startTimeHasPassedBasedOnStopTime - ); - } - /** - * Check if the command is supported - * @param command - message to be checked - * @param pairs - action-target pairs supported by the consumer - * @returns - */ - public static isSupportedAction( - command: Control.CommandMessage, - pairs: Control.ActionTargetPairs - ): boolean { - const action = command.content.action; - const targetType = new RegExp(`^${Accessors.getTargetFromCommandMessage(command)}$`); - const supportedActions = get(pairs, action); - return ( - supportedActions !== undefined && supportedActions.some(target => targetType.test(target)) - ); - } - /** - * Check if the command only request an ack as response - * @param command - message to be checked - * @returns - */ - public static isAckOnlyRequested(command: Control.CommandMessage): boolean { - return get(command, responseRequested) === Control.ResponseType.ACK; - } - /** - * Check if the command is a featured request - * @param command - message to be checked - * @returns - */ - public static isQueryFeaturesRequest(command: Control.CommandMessage): boolean { - const action = command.content.action; - const targetType = Accessors.getTargetFromCommandMessage(command); - return action === Control.Action.Query && targetType === 'features'; - } - /** - * Check if the command is a valid featured request - * @param command - message to be checked - * @returns - */ - public static isValidQueryFeaturesRequest(command: Control.CommandMessage): boolean { - return get(command, responseRequested) === Control.ResponseType.Complete; - } - /** - * Return the delay allowed from command - * @param command - message to be processed - */ - public static isDelayDefinedOnCommand(command: Control.Command): boolean { - const startTimeIsDefined = get(command, 'args.start_time', undefined) ? true : false; - const stopTimeIsDefined = get(command, 'args.stop_time', undefined) ? true : false; - const durationIsDefined = get(command, 'args.duration', undefined) ? true : false; - const basedInDuration = startTimeIsDefined && durationIsDefined; - const basedInStopTime = stopTimeIsDefined; - return basedInStopTime !== basedInDuration; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { get, set } from 'lodash'; +import { Checker } from '../modules'; +import { ConsumerOptions, Control } from '../types'; +import { Accessors } from './Accessors'; +import { Helpers } from './Helpers'; + +const responseRequested = 'content.args.response_requested'; +export class Checkers { + /** + * Check if the message is a valid message. + * @param message - message to be check + * @param uuid - traceability identifier + * @returns Validated message. + * @throws In case of invalid message, throw a validation error. + */ + public static isValidMessageSync(message: Control.Message, uuid: string): Control.Message { + return Checker.attempt('Control.Message', message, uuid); + } + /** + * Check if the message is a valid message. + * @param message - message to be check + * @param uuid - traceability identifier + * @returns Promise with the validated message. + */ + public static isValidMessage(message: Control.Message, uuid: string): Promise { + return Checker.validate('Control.Message', message, uuid); + } + /** + * Check if the command is a valid command message. + * @param command - command to be check + * @param uuid - traceability identifier + * @returns Validated command message. + * @throws In case of invalid command, throw a validation error. + */ + public static isValidCommandSync(command: Control.Message, uuid: string): Control.CommandMessage { + return Checker.attempt('Control.Message.Command', command, uuid); + } + /** + * Check if the command is a valid command message. + * @param command - command to be check + * @param uuid - traceability identifier + * @returns Promise with the validated command message. + */ + public static isValidCommand( + command: Control.Message, + uuid: string + ): Promise { + return Checker.validate('Control.Message.Command', command, uuid); + } + /** + * Check if the response is a valid response message + * @param response - response to be checked + * @param uuid - traceability identifier + * @returns Validated response message. + * @throws In case of invalid response, throw a validation error. + */ + public static isValidResponseSync( + response: Control.Message, + uuid: string + ): Control.ResponseMessage { + return Checker.attempt('Control.Message.Response', response, uuid); + } + /** + * Check if the response is a valid response message + * @param response - response to be checked + * @param uuid - traceability identifier + * @returns Promise with the validated response message. + */ + public static isValidResponse( + response: Control.Message, + uuid: string + ): Promise { + return Checker.validate('Control.Message.Response', response, uuid); + } + /** + * Checks if the command should be response with a default response + * @param command - message to be checked + * @param options - Consumer options + * @returns Default response for the command or undefined if the command has no default response + */ + public static hasDefaultResponse( + command: Control.CommandMessage, + options: ConsumerOptions + ): Control.ResponseMessage | undefined { + if (!this.isOnTime(command)) { + return Helpers.badRequest(command, options.id, 'Command is out of time'); + } else if (this.isQueryFeaturesRequest(command)) { + if (this.isValidQueryFeaturesRequest(command)) { + return Helpers.respondFeatures( + command, + options.id, + options.actionTargetPairs, + options.profiles + ); + } else { + set(command, responseRequested, Control.ResponseType.Complete); + return Helpers.badRequest(command, options.id, 'Invalid Query Features'); + } + } else if (!this.isSupportedAction(command, options.actionTargetPairs)) { + return Helpers.notImplemented(command, options.id, 'Command not supported'); + } else if (this.isAckOnlyRequested(command)) { + return Helpers.processing(command, options.id, undefined, 'Command accepted'); + } else { + return undefined; + } + } + /** + * Check if the message is a command the instance indicated or for all the instances + * @param message - message to be checked + * @param id - instance identification + * @returns true if the message is for this instance or for all the instances + */ + public static isCommandToInstance(message: Control.Message, id: string): boolean { + return ( + message.msg_type === Control.MessageType.Command && + (message.to.includes(id) || message.to.includes('*')) && + message.from !== id + ); + } + /** + * Check if the message is a response for our command + * @param message - message to be checked + * @param from - from field of the original command + * @param requestId - request_id field of the original command + * @returns + */ + public static isResponseToInstance( + message: Control.Message, + from: string, + requestId: string + ): boolean { + return ( + message.msg_type === Control.MessageType.Response && + (message.to.includes(from) || message.to.includes('*')) && + message.request_id === requestId + ); + } + /** + * Check if the command has arguments based on execution time and if we are on time + * @param command - message to be checked + * @returns + */ + public static isOnTime(command: Control.CommandMessage): boolean { + const startTime = get(command, 'content.args.start_time', undefined); + const stopTime = get(command, 'content.args.stop_time', undefined); + const duration = get(command, 'content.args.duration', undefined); + const wrongConfiguration = + startTime !== undefined && stopTime !== undefined && duration !== undefined; + const startTimeHasPassedBasedOnStartTime = + startTime !== undefined && duration !== undefined && startTime + duration < Date.now(); + const startTimeHasPassedBasedOnStopTime = + startTime === undefined && + stopTime !== undefined && + duration !== undefined && + stopTime < Date.now(); + return ( + !wrongConfiguration && + !startTimeHasPassedBasedOnStartTime && + !startTimeHasPassedBasedOnStopTime + ); + } + /** + * Check if the command is supported + * @param command - message to be checked + * @param pairs - action-target pairs supported by the consumer + * @returns + */ + public static isSupportedAction( + command: Control.CommandMessage, + pairs: Control.ActionTargetPairs + ): boolean { + const action = command.content.action; + const targetType = new RegExp(`^${Accessors.getTargetFromCommandMessage(command)}$`); + const supportedActions = get(pairs, action); + return supportedActions?.some(target => targetType.test(target)) ?? false; + } + /** + * Check if the command only request an ack as response + * @param command - message to be checked + * @returns + */ + public static isAckOnlyRequested(command: Control.CommandMessage): boolean { + return get(command, responseRequested) === Control.ResponseType.ACK; + } + /** + * Check if the command is a featured request + * @param command - message to be checked + * @returns + */ + public static isQueryFeaturesRequest(command: Control.CommandMessage): boolean { + const action = command.content.action; + const targetType = Accessors.getTargetFromCommandMessage(command); + return action === Control.Action.Query && targetType === 'features'; + } + /** + * Check if the command is a valid featured request + * @param command - message to be checked + * @returns + */ + public static isValidQueryFeaturesRequest(command: Control.CommandMessage): boolean { + return get(command, responseRequested) === Control.ResponseType.Complete; + } + /** + * Return the delay allowed from command + * @param command - message to be processed + */ + public static isDelayDefinedOnCommand(command: Control.Command): boolean { + const startTimeIsDefined = Boolean(get(command, 'args.start_time', undefined)); + const stopTimeIsDefined = Boolean(get(command, 'args.stop_time', undefined)); + const durationIsDefined = Boolean(get(command, 'args.duration', undefined)); + const basedInDuration = startTimeIsDefined && durationIsDefined; + const basedInStopTime = stopTimeIsDefined; + return basedInStopTime !== basedInDuration; + } +} diff --git a/packages/api/openc2-core/src/index.ts b/packages/api/openc2-core/src/index.ts index 2b51ee68..aed2b4d0 100644 --- a/packages/api/openc2-core/src/index.ts +++ b/packages/api/openc2-core/src/index.ts @@ -1,23 +1,24 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -export { Consumer, Gateway, Producer } from './components'; -export { Accessors } from './helpers'; -export { Registry } from './modules'; -export { - CommandJobDone, - CommandJobHandler, - ConsumerAdapter, - ConsumerOptions, - Control, - GatewayOptions, - OnCommandHandler, - ProducerAdapter, - ProducerOptions, - Resolver, - ResolverEntry, - ResolverMap, -} from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +export { Consumer, ConsumerMap, Gateway, Producer } from './components'; +export { Accessors } from './helpers'; +export { Registry } from './modules'; +export { + CommandJobDone, + CommandJobHandler, + CommandJobHeader, + ConsumerAdapter, + ConsumerOptions, + Control, + GatewayOptions, + OnCommandHandler, + ProducerAdapter, + ProducerOptions, + Resolver, + ResolverEntry, + ResolverMap, +} from './types'; diff --git a/packages/api/tasks/README.md b/packages/api/tasks/README.md index f536177e..153bc209 100644 --- a/packages/api/tasks/README.md +++ b/packages/api/tasks/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/tasks/package.json b/packages/api/tasks/package.json index 89e73cbb..3983db61 100644 --- a/packages/api/tasks/package.json +++ b/packages/api/tasks/package.json @@ -1,55 +1,54 @@ -{ - "name": "@mdf.js/tasks", - "version": "0.0.1", - "description": "MMS - API Core - Tasks", - "keywords": [ - "NodeJS", - "MMS", - "API", - "Tasks" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/task" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "doc": "typedoc --options typedoc.json", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/core/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "lodash": "^4.17.21", - "ms": "^2.1.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/ms": "^0.7.34", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/tasks", + "version": "0.0.1", + "description": "MMS - API Core - Tasks", + "keywords": [ + "NodeJS", + "MMS", + "API", + "Tasks" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/task" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "doc": "typedoc --options typedoc.json", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/core/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "lodash": "^4.17.21", + "ms": "^2.1.3", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13", + "@types/ms": "^0.7.34" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/tasks/src/Helpers/Validator.ts b/packages/api/tasks/src/Helpers/Validator.ts index 9f556ad8..762e2614 100644 --- a/packages/api/tasks/src/Helpers/Validator.ts +++ b/packages/api/tasks/src/Helpers/Validator.ts @@ -1,262 +1,240 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import ms from 'ms'; -import { - DefaultPollingGroups, - GroupTaskBaseConfig, - PollingGroup, - SequenceTaskBaseConfig, - SingleTaskBaseConfig, - TaskBaseConfig, - WellIdentifiedTaskOptions, -} from '../Polling'; -import { ResourceConfigEntry, ResourcesConfigObject } from '../Scheduler'; - -export class Validator { - /** - * Validate the resources configuration - * @param resources - The resources configuration - */ - public static validateResources< - Result = any, - Binding = any, - PollingGroups extends PollingGroup = DefaultPollingGroups, - >(resources: ResourcesConfigObject): void { - if (!resources || typeof resources !== 'object' || Array.isArray(resources)) { - throw new Crash(`The resources should be an object: ${JSON.stringify(resources, null, 2)}`); - } - for (const [resource, entry] of Object.entries(resources)) { - if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { - throw new Crash( - `The resource entry should be an object: ${JSON.stringify(entry, null, 2)}` - ); - } - if (typeof resource !== 'string' || resource.length < 1) { - throw new Crash( - `The resource should be a non empty string: ${JSON.stringify(resource, null, 2)}` - ); - } - Validator.valideEntry(entry); - } - } - /** - * Check if the entry is valid - * @param entry - The entry to add to the scheduler - */ - public static valideEntry< - Result = any, - Binding = any, - PollingGroups extends PollingGroup = DefaultPollingGroups, - >(entry: ResourceConfigEntry): void { - for (const [polling, tasks] of Object.entries(entry.pollingGroups)) { - Validator.isValidPeriod(polling); - if (!Array.isArray(tasks)) { - throw new Crash(`The tasks should be an array of tasks: ${JSON.stringify(tasks, null, 2)}`); - } - for (const task of tasks) { - Validator.isValidConfig(task); - } - } - } - /** - * Check if the task configuration is a single task configuration - * @param config - The task configuration - */ - public static isSingleTaskConfig(config: TaskBaseConfig): config is SingleTaskBaseConfig { - try { - Validator.isValidConfig(config); - return 'task' in config; - } catch { - return false; - } - } - /** - * Check if the task configuration is a group task configuration - * @param config - The task configuration - */ - public static isGroupTaskConfig(config: TaskBaseConfig): config is GroupTaskBaseConfig { - try { - Validator.isValidConfig(config); - return 'tasks' in config; - } catch { - return false; - } - } - /** - * Check if the task configuration is a sequence task configuration - * @param config - The task configuration - */ - public static isSequenceTaskConfig(config: TaskBaseConfig): config is SequenceTaskBaseConfig { - try { - Validator.isValidConfig(config); - return 'pattern' in config; - } catch { - return false; - } - } - /** Check if the period is valid */ - private static isValidPeriod(period: string): void { - if (typeof period !== 'string' || period.length < 2 || !Validator.isValidEndForPeriod(period)) { - throw new Crash(`The period should be a string with the format `); - } else { - try { - const value = ms(period); - if (typeof value !== 'number' || Number.isNaN(value) || value < 0) { - throw new Crash(`Wrong period value [${period}]`); - } - } catch (rawError) { - const error = Crash.from(rawError); - throw new Crash( - `The period could not be parsed: ${error.message}, the period should be a string with the format ` - ); - } - } - } - /** - * Check if the period is based on milliseconds, seconds, minutes, hours or days - * @param period - The period to check - * @returns - True if the period is valid - */ - private static isValidEndForPeriod(period: string): boolean { - return ( - period.endsWith('ms') || - period.endsWith('s') || - period.endsWith('m') || - period.endsWith('h') || - period.endsWith('d') - ); - } - /** Check if the configuration is valid */ - private static isValidConfig(config: TaskBaseConfig): void { - if (!config || typeof config !== 'object' || Array.isArray(config)) { - throw new Crash(`The task configuration should be an object`); - } else { - if ('task' in config) { - Validator.isValidSingleTaskConfig(config as SingleTaskBaseConfig); - } else if ('tasks' in config) { - Validator.isValidGroupConfig(config as GroupTaskBaseConfig); - } else if ('pattern' in config) { - Validator.isValidSequenceConfig(config as SequenceTaskBaseConfig); - } else { - throw new Crash(`The task configuration should have a task, tasks or pattern property`); - } - } - } - /** - * Check if the task configuration is valid - * @param config - The task configuration - */ - private static isValidSingleTaskConfig(config: SingleTaskBaseConfig): void { - if (typeof config.task !== 'function') { - throw new Crash( - `The task should be a function or a promise: ${JSON.stringify(config, null, 2)}` - ); - } else if ('taskArgs' in config && !Array.isArray(config.taskArgs)) { - throw new Crash(`The taskArgs should be an array: ${JSON.stringify(config, null, 2)}`); - } else { - Validator.isValidTaskOptions(config.options); - } - } - /** - * Check if the task options are valid - * @param options - The task options - */ - private static isValidTaskOptions(options: WellIdentifiedTaskOptions): void { - if (!options || typeof options !== 'object' || Array.isArray(options)) { - throw new Crash(`The options should be an object: ${JSON.stringify(options, null, 2)}`); - } else if (!('id' in options)) { - throw new Crash( - `The options should have an id property: ${JSON.stringify(options, null, 2)}` - ); - } else if (typeof options.id !== 'string') { - throw new Crash(`The id should be a string: ${JSON.stringify(options, null, 2)}`); - } else if (options.id.length < 1) { - throw new Crash(`The id should be a non empty string: ${JSON.stringify(options, null, 2)}`); - } else if (options.id.length > 255) { - throw new Crash( - `The id should be a string with less than 255 characters: ${JSON.stringify(options, null, 2)}` - ); - } - } - /** - * Check if the group configuration is valid - * @param config - The group configuration - */ - private static isValidGroupConfig(config: GroupTaskBaseConfig): void { - if (!Array.isArray((config as GroupTaskBaseConfig).tasks)) { - throw new Crash(`The tasks should be an array of tasks: ${JSON.stringify(config, null, 2)}`); - } else { - for (const task of (config as GroupTaskBaseConfig).tasks) { - Validator.isValidSingleTaskConfig(task); - } - Validator.isValidTaskOptions(config.options); - } - } - /** - * Check if the sequence configuration is valid - * @param config - The sequence configuration - */ - private static isValidSequenceConfig(config: SequenceTaskBaseConfig): void { - if ( - !('pattern' in config) || - !config.pattern || - typeof config.pattern !== 'object' || - Array.isArray(config.pattern) - ) { - throw new Crash( - `Pattern should be an object an object with the task property: ${JSON.stringify( - config, - null, - 2 - )}` - ); - } else if ( - !('task' in config.pattern) || - !config.pattern.task || - typeof config.pattern.task !== 'object' || - Array.isArray(config.pattern.task) - ) { - throw new Crash( - `The sequence configuration should have a task property: ${JSON.stringify(config, null, 2)}` - ); - } else if ('pre' in config.pattern && !Array.isArray(config.pattern.pre)) { - throw new Crash( - `The pre property should be an array of tasks: ${JSON.stringify(config, null, 2)}` - ); - } else if ('post' in config.pattern && !Array.isArray(config.pattern.post)) { - throw new Crash( - `The post property should be an array of tasks: ${JSON.stringify(config, null, 2)}` - ); - } else if ('finally' in config.pattern && !Array.isArray(config.pattern.finally)) { - throw new Crash( - `The finally property should be an array of tasks: ${JSON.stringify(config, null, 2)}` - ); - } else { - if ('task' in config.pattern) { - Validator.isValidSingleTaskConfig(config.pattern.task); - } - if ('pre' in config.pattern && Array.isArray(config.pattern.pre)) { - for (const task of config.pattern.pre) { - Validator.isValidSingleTaskConfig(task); - } - } - if ('post' in config.pattern && Array.isArray(config.pattern.post)) { - for (const task of config.pattern.post) { - Validator.isValidSingleTaskConfig(task); - } - } - if ('finally' in config.pattern && Array.isArray(config.pattern.finally)) { - for (const task of config.pattern.finally) { - Validator.isValidSingleTaskConfig(task); - } - } - Validator.isValidTaskOptions(config.options); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import ms from 'ms'; +import { + DefaultPollingGroups, + GroupTaskBaseConfig, + PollingGroup, + SequenceTaskBaseConfig, + SingleTaskBaseConfig, + TaskBaseConfig, + WellIdentifiedTaskOptions, +} from '../Polling'; +import { ResourceConfigEntry, ResourcesConfigObject } from '../Scheduler'; + +export class Validator { + /** + * Validate the resources configuration + * @param resources - The resources configuration + */ + public static validateResources< + Result = any, + Binding = any, + PollingGroups extends PollingGroup = DefaultPollingGroups, + >(resources: ResourcesConfigObject): void { + if (!resources || typeof resources !== 'object' || Array.isArray(resources)) { + throw new Crash(`The resources should be an object: ${JSON.stringify(resources, null, 2)}`); + } + for (const [resource, entry] of Object.entries(resources)) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + throw new Crash( + `The resource entry should be an object: ${JSON.stringify(entry, null, 2)}` + ); + } + if (typeof resource !== 'string' || resource.length < 1) { + throw new Crash( + `The resource should be a non empty string: ${JSON.stringify(resource, null, 2)}` + ); + } + Validator.valideEntry(entry); + } + } + /** + * Check if the entry is valid + * @param entry - The entry to add to the scheduler + */ + public static valideEntry< + Result = any, + Binding = any, + PollingGroups extends PollingGroup = DefaultPollingGroups, + >(entry: ResourceConfigEntry): void { + for (const [polling, tasks] of Object.entries(entry.pollingGroups)) { + Validator.isValidPeriod(polling); + if (!Array.isArray(tasks)) { + throw new Crash(`The tasks should be an array of tasks: ${JSON.stringify(tasks, null, 2)}`); + } + for (const task of tasks) { + Validator.isValidConfig(task); + } + } + } + /** + * Check if the task configuration is a single task configuration + * @param config - The task configuration + */ + public static isSingleTaskConfig(config: TaskBaseConfig): config is SingleTaskBaseConfig { + try { + Validator.isValidConfig(config); + return 'task' in config; + } catch { + return false; + } + } + /** + * Check if the task configuration is a group task configuration + * @param config - The task configuration + */ + public static isGroupTaskConfig(config: TaskBaseConfig): config is GroupTaskBaseConfig { + try { + Validator.isValidConfig(config); + return 'tasks' in config; + } catch { + return false; + } + } + /** + * Check if the task configuration is a sequence task configuration + * @param config - The task configuration + */ + public static isSequenceTaskConfig(config: TaskBaseConfig): config is SequenceTaskBaseConfig { + try { + Validator.isValidConfig(config); + return 'pattern' in config; + } catch { + return false; + } + } + /** Check if the period is valid */ + private static isValidPeriod(period: string): void { + if (typeof period !== 'string' || period.length < 2 || !Validator.isValidEndForPeriod(period)) { + throw new Crash(`The period should be a string with the format `); + } else { + try { + const value = ms(period); + if (typeof value !== 'number' || Number.isNaN(value) || value < 0) { + throw new Crash(`Wrong period value [${period}]`); + } + } catch (rawError) { + const error = Crash.from(rawError); + throw new Crash( + `The period could not be parsed: ${error.message}, the period should be a string with the format ` + ); + } + } + } + /** + * Check if the period is based on milliseconds, seconds, minutes, hours or days + * @param period - The period to check + * @returns - True if the period is valid + */ + private static isValidEndForPeriod(period: string): boolean { + return ( + period.endsWith('ms') || + period.endsWith('s') || + period.endsWith('m') || + period.endsWith('h') || + period.endsWith('d') + ); + } + /** Check if the configuration is valid */ + private static isValidConfig(config: TaskBaseConfig): void { + if (!config || typeof config !== 'object' || Array.isArray(config)) { + throw new Crash(`The task configuration should be an object`); + } else if ('task' in config) { + Validator.isValidSingleTaskConfig(config); + } else if ('tasks' in config) { + Validator.isValidGroupConfig(config); + } else if ('pattern' in config) { + Validator.isValidSequenceConfig(config); + } else { + throw new Crash(`The task configuration should have a task, tasks or pattern property`); + } + } + /** + * Check if the task configuration is valid + * @param config - The task configuration + */ + private static isValidSingleTaskConfig(config: SingleTaskBaseConfig): void { + if (typeof config.task !== 'function') { + throw new Crash( + `The task should be a function or a promise: ${JSON.stringify(config, null, 2)}` + ); + } else if ('taskArgs' in config && !Array.isArray(config.taskArgs)) { + throw new Crash(`The taskArgs should be an array: ${JSON.stringify(config, null, 2)}`); + } else { + Validator.isValidTaskOptions(config.options); + } + } + /** + * Check if the task options are valid + * @param options - The task options + */ + private static isValidTaskOptions(options: WellIdentifiedTaskOptions): void { + if (!options || typeof options !== 'object' || Array.isArray(options)) { + throw new Crash(`The options should be an object: ${JSON.stringify(options, null, 2)}`); + } else if (!('id' in options)) { + throw new Crash( + `The options should have an id property: ${JSON.stringify(options, null, 2)}` + ); + } else if (typeof options.id !== 'string') { + throw new Crash(`The id should be a string: ${JSON.stringify(options, null, 2)}`); + } else if (options.id.length < 1) { + throw new Crash(`The id should be a non empty string: ${JSON.stringify(options, null, 2)}`); + } else if (options.id.length > 255) { + throw new Crash( + `The id should be a string with less than 255 characters: ${JSON.stringify(options, null, 2)}` + ); + } + } + /** + * Check if the group configuration is valid + * @param config - The group configuration + */ + private static isValidGroupConfig(config: GroupTaskBaseConfig): void { + if (!Array.isArray(config.tasks)) { + throw new Crash(`The tasks should be an array of tasks: ${JSON.stringify(config, null, 2)}`); + } else { + for (const task of config.tasks) { + Validator.isValidSingleTaskConfig(task); + } + Validator.isValidTaskOptions(config.options); + } + } + /** + * Check if the sequence configuration is valid + * @param config - The sequence configuration + */ + private static isValidSequenceConfig(config: SequenceTaskBaseConfig): void { + // Helper to validate array properties + const validateArrayProperty = (property: 'pre' | 'post' | 'finally') => { + if (property in config.pattern && !Array.isArray(config.pattern[property])) { + throw new Crash( + `The ${property} property should be an array of tasks: ${JSON.stringify(config, null, 2)}` + ); + } + }; + // Ensure `pattern` is a valid object with a `task` property + if (!config.pattern || typeof config.pattern !== 'object' || Array.isArray(config.pattern)) { + throw new Crash( + `Pattern should be an object with the task property: ${JSON.stringify(config, null, 2)}` + ); + } + if ( + !config.pattern.task || + typeof config.pattern.task !== 'object' || + Array.isArray(config.pattern.task) + ) { + throw new Crash( + `The sequence configuration should have a task property: ${JSON.stringify(config, null, 2)}` + ); + } + // Validate that `pre`, `post`, and `finally` properties (if present) are arrays + (['pre', 'post', 'finally'] as const).forEach(validateArrayProperty); + // Validate each array of tasks in `pre`, `post`, and `finally` if they exist + (['pre', 'post', 'finally'] as const).forEach(property => { + if (Array.isArray(config.pattern[property])) { + for (const task of config.pattern[property]) { + Validator.isValidSingleTaskConfig(task); + } + } + }); + // Validate any additional task options + Validator.isValidTaskOptions(config.options); + } +} diff --git a/packages/api/tasks/src/Limiter/Limiter.test.ts b/packages/api/tasks/src/Limiter/Limiter.test.ts index 0091fa5f..645097a0 100644 --- a/packages/api/tasks/src/Limiter/Limiter.test.ts +++ b/packages/api/tasks/src/Limiter/Limiter.test.ts @@ -1,934 +1,934 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { randomInt } from 'crypto'; -import { setTimeout as promiseSetTimeout } from 'timers/promises'; -import { Group, MetaData, Sequence, SequencePattern, Single } from '../Tasks'; -import { Limiter } from './Limiter'; -import { STRATEGY } from './types'; - -const fixture = Symbol('fixture'); - -describe('#Limiter', () => { - describe('#Happy path', () => { - beforeEach(() => {}); - afterEach(() => {}); - it(`Simple test for Limiter`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.schedule(async () => results.push(1), [], { id: 'myId1' }); - queue.schedule(async () => results.push(2)); - queue.schedule(async () => results.push(3)); - const test4 = await queue.execute(async () => results.push(4)); - const test5 = await queue.execute(async () => results.push(5)); - expect(test4).toEqual(4); - expect(test5).toEqual(5); - expect(results).toEqual([1, 2, 3, 4, 5]); - expect(queue.options.concurrency).toEqual(1); - }); - it(`Simple test for Limiter, using piped limiter`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const piped = new Limiter({ concurrency: 1 }); - queue.pipe(piped); - const results: number[] = []; - let id1OnQueue = false; - let id4OnQueue = false; - let id1OnPiped = false; - let id4OnPiped = false; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - id1OnQueue = true; - }); - queue.on('myId4', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(4); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - id4OnQueue = true; - }); - piped.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - piped.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - id1OnPiped = true; - }); - piped.on('myId4', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(4); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - id4OnPiped = true; - }); - queue.schedule(async () => results.push(4), [], { id: 'myId4' }); - queue.schedule(async () => results.push(5)); - queue.schedule(async () => results.push(6)); - piped.schedule(async () => results.push(1), [], { id: 'myId1' }); - piped.schedule(async () => results.push(2)); - piped.schedule(async () => results.push(3)); - const test7 = await queue.execute(async () => results.push(7)); - const test8 = await queue.execute(async () => results.push(8)); - expect(test7).toEqual(7); - expect(test8).toEqual(8); - expect(results).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); - expect(id1OnQueue).toBeFalsy(); - expect(id4OnQueue).toBeTruthy(); - expect(id1OnPiped).toBeTruthy(); - expect(id4OnPiped).toBeTruthy(); - }); - it(`Simple test for Limiter, using task single handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.schedule(new Single(async () => results.push(1), [], { id: 'myId1' })); - queue.schedule(new Single(async () => results.push(2))); - queue.schedule(new Single(async () => results.push(3))); - const test4 = await queue.execute(new Single(async () => results.push(4))); - const test5 = await queue.execute(new Single(async () => results.push(5))); - expect(test4).toEqual(4); - expect(test5).toEqual(5); - expect(results).toEqual([1, 2, 3, 4, 5]); - }); - it(`Simple test for Limiter, using task groups handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - const scheduleTasks = [ - new Single(async () => results.push(1), [], { id: 'myId1' }), - new Single(async () => results.push(2)), - new Single(async () => results.push(3)), - ]; - const executeTasks = [ - new Single(async () => results.push(4)), - new Single(async () => results.push(5)), - ]; - queue.schedule(new Group(scheduleTasks)); - const test = await queue.execute(new Group(executeTasks)); - expect(test).toEqual([4, 5]); - expect(results).toEqual([1, 2, 3, 4, 5]); - }); - it(`Simple test for Limiter, using task sequence handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - const sequence: SequencePattern = { - pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], - task: new Single(async () => results.push(3)), - post: [new Single(async () => results.push(4))], - finally: [new Single(async () => results.push(5))], - }; - const test = await queue.execute(new Sequence(sequence, { id: 'myId1' })); - expect(test).toEqual(3); - expect(results).toEqual([1, 2, 3, 4, 5]); - }); - it(`Simple test for Limiter with delay`, async () => { - const queue = new Limiter({ concurrency: 1, delay: 100 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(1); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - const hrstart = process.hrtime(); - queue.schedule(async () => results.push(1), [], { id: 'myId1' }); - queue.schedule(async () => results.push(2)); - queue.schedule(async () => results.push(3)); - await queue.waitUntilEmpty(); - const hrend = process.hrtime(hrstart); - expect(hrend[0]).toBe(0); - const end = () => hrend[1] / 1000000; - expect(end()).toBeGreaterThan(200); - expect(end()).toBeLessThan(220); - expect(results).toEqual([1, 2, 3]); - }); - it(`Simple test for Limiter with delay and concurrency`, async () => { - const queue = new Limiter({ concurrency: 2, delay: 100 }); - const results: number[] = []; - const hrstart = process.hrtime(); - await queue.execute(async () => results.push(1), [], { id: 'myId1' }); - await queue.execute(async () => results.push(2)); - await queue.execute(async () => results.push(3)); - const hrend = process.hrtime(hrstart); - expect(hrend[0]).toBe(0); - const end = () => hrend[1] / 1000000; - expect(end()).toBeGreaterThan(200); - expect(end()).toBeLessThan(220); - expect(results).toEqual([1, 2, 3]); - }); - it('.execute() should resolve', async () => { - const queue = new Limiter(); - const result = await queue.execute(async () => fixture); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect(result).toEqual(fixture); - }); - it('.execute() should reject if promise rejects', async () => { - const queue = new Limiter(); - try { - await queue.execute( - async () => { - throw new Error('test'); - }, - [], - { id: 'myId', retryOptions: { attempts: 1 } } - ); - } catch (error) { - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('test'); - } - }); - it('.execute() should reject if promise rejects, with single task handlers without bind', async () => { - const queue = new Limiter(); - try { - const single = new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId', retryOptions: { attempts: 1 } } - ); - await queue.execute(single); - } catch (error) { - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('test'); - } - }); - it('.execute() should reject if promise rejects, with single task handlers with bind', async () => { - const queue = new Limiter(); - class myClass { - public async myMethod() { - throw new Error('test'); - } - } - const myInstance = new myClass(); - try { - const single = new Single(myInstance.myMethod, { - id: 'myId', - retryOptions: { attempts: 1 }, - bind: myInstance, - }); - await queue.execute(single); - } catch (error) { - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('test'); - } - }); - it('.execute() should reject if promise rejects, with group task handlers', async () => { - const queue = new Limiter(); - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - try { - const group = new Group( - [ - new Single(async () => fixture), - new Single(async () => fixture), - new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId' } - ), - new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId' } - ), - ], - { id: 'myId' } - ); - await queue.execute(group); - } catch (error) { - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - const message = - 'Execution error in task [myId]: CrashError: Execution error in task [myId]: test,\ncaused by InterruptionError: Too much attempts [1], the promise will not be retried,\ncaused by Error: test,\nCrashError: Execution error in task [myId]: test,\ncaused by InterruptionError: Too much attempts [1], the promise will not be retried,\ncaused by Error: test'; - expect((error as Crash).message).toEqual(message); - const cause = (error as Crash).cause as Multi; - expect(cause.message).toEqual(`At least one of the task grouped failed`); - const internalErrors = cause.causes as Crash[]; - expect(internalErrors).toBeDefined(); - expect(internalErrors[0].message).toEqual('Execution error in task [myId]: test'); - expect(internalErrors[1].message).toEqual('Execution error in task [myId]: test'); - } - }); - it(`.execute() should reject if task promise rejects, using task sequence handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - const sequence: SequencePattern = { - pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], - task: new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId', retryOptions: { attempts: 1 } } - ), - post: [new Single(async () => results.push(4))], - finally: [new Single(async () => results.push(5))], - }; - try { - await queue.execute(new Sequence(sequence, { id: 'myId1' })); - } catch (error) { - expect(results).toEqual([1, 2, 5]); - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual( - 'Execution error in task [myId1]: Execution error in task [myId]: test' - ); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual(`Execution error in task [myId]: test`); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual( - 'Too much attempts [1], the promise will not be retried' - ); - } - }); - it(`.execute() should reject if pre task promise rejects, using task sequence handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - const sequence: SequencePattern = { - pre: [ - new Single(async () => results.push(1)), - new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId', retryOptions: { attempts: 1 } } - ), - ], - task: new Single(async () => results.push(2)), - post: [new Single(async () => results.push(3))], - finally: [new Single(async () => results.push(4))], - }; - try { - await queue.execute(new Sequence(sequence, { id: 'myId1' })); - } catch (error) { - expect(results).toEqual([1, 4]); - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual( - 'Execution error in task [myId1]: Error executing the [pre] phase: Execution error in task [myId]: test' - ); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual( - `Error executing the [pre] phase: Execution error in task [myId]: test` - ); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('Execution error in task [myId]: test'); - expect(internalError.cause).toBeDefined(); - expect(internalError.cause?.message).toEqual( - 'Too much attempts [1], the promise will not be retried' - ); - } - }); - it(`.execute() should reject if post task promise rejects, using task sequence handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - const sequence: SequencePattern = { - pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], - task: new Single(async () => results.push(3)), - post: [ - new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId', retryOptions: { attempts: 1 } } - ), - ], - finally: [new Single(async () => results.push(4))], - }; - try { - await queue.execute(new Sequence(sequence, { id: 'myId1' })); - } catch (error) { - expect(results).toEqual([1, 2, 3, 4]); - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toEqual( - 'Execution error in task [myId1]: Error executing the [post] phase: Execution error in task [myId]: test' - ); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual( - `Error executing the [post] phase: Execution error in task [myId]: test` - ); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('Execution error in task [myId]: test'); - expect(internalError.cause).toBeDefined(); - expect(internalError.cause?.message).toEqual( - 'Too much attempts [1], the promise will not be retried' - ); - } - }); - it(`.execute() should reject if final task promise rejects, using task sequence handlers`, async () => { - const queue = new Limiter({ concurrency: 1 }); - const results: number[] = []; - const sequence: SequencePattern = { - pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], - task: new Single(async () => results.push(3)), - post: [new Single(async () => results.push(4))], - finally: [ - new Single( - async () => { - throw new Error('test'); - }, - { id: 'myId', retryOptions: { attempts: 1 } } - ), - ], - }; - let test; - try { - test = await queue.execute(new Sequence(sequence, { id: 'myId1' })); - } catch (error) { - expect(test).toEqual(undefined); - expect(results).toEqual([1, 2, 3, 4]); - expect(error).toBeDefined(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect((error as Multi).message).toEqual( - 'Execution error in task [myId1]: Error executing the [finally] phase: Execution error in task [myId]: test' - ); - const cause = (error as Crash).cause as Crash; - expect(cause.message).toEqual( - `Error executing the [finally] phase: Execution error in task [myId]: test` - ); - const internalError = cause.cause as Crash; - expect(internalError).toBeDefined(); - expect(internalError.message).toEqual('Execution error in task [myId]: test'); - } - }); - it(`.execute() should reject if the queue is full`, async () => { - const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW }); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - try { - await queue.execute(async () => fixture); - } catch (error) { - expect(error).toBeDefined(); - expect(queue.size).toBe(2); - expect(queue.pending).toBe(0); - expect((error as Crash).message).toContain('Execution error in task'); - expect((error as Crash).message).toContain('The job could not be scheduled'); - } - }); - it('.schedule() - limited concurrency', async () => { - const queue = new Limiter({ concurrency: 2 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toEqual(fixture); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - results.push(result); - }); - queue.schedule(async () => fixture, [], { id: 'myId' }); - queue.schedule( - async () => { - await promiseSetTimeout(100); - return fixture; - }, - [], - { id: 'myId' } - ); - expect(queue.size).toBe(2); - expect(queue.pending).toBe(0); - queue.schedule(async () => fixture, [], { id: 'myId' }); - expect(queue.size).toBe(3); - expect(queue.pending).toBe(0); - const test = await queue.execute(async () => fixture, [], { id: 'myId' }); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - expect(test).toEqual(fixture); - await queue.waitUntilEmpty(); - expect(results).toEqual([fixture, fixture, fixture, fixture]); - }); - it(`.schedule() - concurrency: 1`, async () => { - const input = [ - [10, 50], - [20, 33], - [30, 17], - ]; - const results: number[] = []; - const hrstart = process.hrtime(); - const queue = new Limiter({ concurrency: 1 }); - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - results.push(result); - }); - const mapper = async ([value, ms]: readonly number[]) => - queue.schedule( - async () => { - await promiseSetTimeout(ms!); - return value!; - }, - [], - { id: 'myId' } - ); - input.map(mapper); - await queue.waitUntilEmpty(); - const hrend = process.hrtime(hrstart); - expect(hrend[0]).toBe(0); - const end = () => hrend[1] / 1000000; - expect(end()).toBeGreaterThan(90); - expect(end()).toBeLessThan(120); - expect(results).toEqual([10, 20, 30]); - }); - it(`.execute() - concurrency: 3`, async () => { - const concurrency = 3; - const queue = new Limiter({ concurrency, delay: 0 }); - let running = 0; - let index = 0; - const results: number[] = []; - const input = Array.from({ length: 100 }) - .fill(0) - .map(() => - queue.execute(async () => { - running++; - expect(running).toBeLessThanOrEqual(concurrency); - expect(queue.pending).toBeLessThanOrEqual(concurrency); - await promiseSetTimeout(randomInt(5, 15)); - results.push(index++); - running--; - }) - ); - await Promise.all(input); - expect(results).toHaveLength(100); - }); - it(`.schedule() - concurrency: 3`, async () => { - const concurrency = 3; - const queue = new Limiter({ concurrency, delay: 0 }); - let running = 0; - let index = 0; - const results: number[] = []; - Array.from({ length: 100 }) - .fill(0) - .map(() => - queue.schedule(async () => { - running++; - expect(running).toBeLessThanOrEqual(concurrency); - expect(queue.pending).toBeLessThanOrEqual(concurrency); - await promiseSetTimeout(randomInt(5, 15)); - results.push(index++); - running--; - }) - ); - await queue.waitUntilEmpty(); - expect(results).toHaveLength(100); - }); - it(`.schedule() - priority`, async () => { - const results: number[] = []; - const queue = new Limiter({ concurrency: 1 }); - queue.schedule(async () => results.push(1), [], { priority: 1, id: 'myId1-1' }); - queue.schedule(async () => results.push(0), [], { priority: 0, id: 'myId0-1' }); - queue.schedule(async () => results.push(1), [], { priority: 1, id: 'myId1-2' }); - queue.schedule(async () => results.push(2), [], { priority: 2, id: 'myId2-1' }); - queue.schedule(async () => results.push(3), [], { priority: 3, id: 'myId3-1' }); - queue.schedule(async () => results.push(0), [], { priority: -1, id: 'myId0-2' }); - await queue.waitUntilEmpty(); - expect(results).toEqual([3, 2, 1, 1, 0, 0]); - }); - it(`.clear()`, async () => { - const queue = new Limiter({ concurrency: 2, autoStart: false }); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - expect(queue.size).toBe(4); - expect(queue.pending).toBe(0); - queue.clear(); - expect(queue.size).toBe(0); - }); - it(`autoStart: false`, async () => { - const queue = new Limiter({ concurrency: 2, autoStart: false }); - - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - queue.schedule(async () => fixture); - expect(queue.size).toBe(4); - expect(queue.pending).toBe(0); - - queue.start(); - await queue.waitUntilEmpty(); - expect(queue.size).toBe(0); - expect(queue.pending).toBe(0); - }); - it(`.stop()`, async () => { - const queue = new Limiter({ concurrency: 2, autoStart: false }); - - queue.schedule(async () => promiseSetTimeout(100)); - queue.schedule(async () => promiseSetTimeout(100)); - queue.schedule(async () => promiseSetTimeout(100)); - queue.schedule(async () => promiseSetTimeout(100)); - queue.schedule(async () => promiseSetTimeout(100)); - expect(queue.size).toBe(5); - expect(queue.pending).toBe(0); - - queue.start(); - expect(queue.size).toBe(3); - expect(queue.pending).toBe(2); - queue.stop(); - await promiseSetTimeout(200); - expect(queue.size).toBe(3); - expect(queue.pending).toBe(0); - - queue.schedule(async () => promiseSetTimeout(100)); - expect(queue.size).toBe(4); - expect(queue.pending).toBe(0); - - queue.start(); - expect(queue.size).toBe(2); - expect(queue.pending).toBe(2); - queue.stop(); - queue.clear(); - expect(queue.size).toBe(0); - }); - it(`Should apply strategy 'block'`, async () => { - let blocked = false; - let unblocked = false; - const queue = new Limiter({ highWater: 2, strategy: STRATEGY.BLOCK, penalty: 100 }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - results.push(result); - }); - // @ts-expect-error - testing invalid assignment - queue.queue.on('blocked', () => { - // @ts-expect-error - testing invalid assignment - expect(queue.queue.blocked).toBe(true); - blocked = true; - }); - // @ts-expect-error - testing invalid assignment - queue.queue.on('unblocked', () => { - // @ts-expect-error - testing invalid assignment - expect(queue.queue.blocked).toBe(false); - unblocked = true; - }); - queue.schedule(async () => results.push(1), [], { id: 'myId' }); - expect(queue.size).toBe(1); - queue.schedule(async () => results.push(2), [], { id: 'myId' }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(3), [], { id: 'myId' }); - expect(queue.size).toBe(0); - queue.schedule(async () => results.push(4), [], { id: 'myId' }); - expect(queue.size).toBe(0); - expect(blocked).toBe(true); - await promiseSetTimeout(100); - expect(unblocked).toBe(true); - expect(queue.size).toBe(0); - queue.schedule(async () => results.push(1), [], { id: 'myId' }); - expect(queue.size).toBe(1); - }); - it(`Should apply strategy 'leak'`, async () => { - const queue = new Limiter({ highWater: 2, strategy: STRATEGY.LEAK }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.schedule(async () => results.push(1), [], { id: 'myId1' }); - queue.schedule(async () => results.push(2), [], { id: 'myId2' }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(3), [], { id: 'myId3' }); - expect(queue.size).toBe(2); - await queue.waitUntilEmpty(); - expect(queue.size).toBe(0); - expect(results).toEqual([2, 3]); - }); - it(`Should apply strategy 'overflow-priority'`, async () => { - const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW_PRIORITY }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - queue.schedule(async () => results.push(1), [], { id: 'myId1', priority: 9 }); - queue.schedule(async () => results.push(2), [], { id: 'myId2', priority: 1 }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(3), [], { id: 'myId3', priority: 5 }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(4), [], { id: 'myId4', priority: 5 }); - await queue.waitUntilEmpty(); - expect(queue.size).toBe(0); - expect(results).toEqual([1, 3]); - }); - it(`Should apply strategy 'overflow'`, async () => { - const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - if (error) { - expect(error).toBeDefined(); - expect(error.message).toContain('The job could not be scheduled'); - } - }); - queue.schedule(async () => results.push(1), [], { id: 'myId1', priority: 0 }); - queue.schedule(async () => results.push(2), [], { id: 'myId2', priority: 0 }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(3), [], { id: 'myId3', priority: 9 }); - expect(queue.size).toBe(2); - queue.schedule(async () => results.push(4), [], { id: 'myId4', priority: 9 }); - expect(queue.size).toBe(2); - await queue.waitUntilEmpty(); - expect(queue.size).toBe(0); - expect(results).toEqual([1, 2]); - }); - it(`Should use bucketSize, tokensPerInterval and interval to control the jobs schedule`, async () => { - const queue = new Limiter({ - bucketSize: 3, - tokensPerInterval: 1, - interval: 100, - delay: 0, - concurrency: 3, - }); - const results: number[] = []; - queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { - expect(uuid).toBeDefined(); - expect(result).toBeDefined(); - expect(meta).toBeDefined(); - expect(error).toBeUndefined(); - }); - const hrstart = process.hrtime(); - const mapper = async (value: number) => queue.schedule(async () => results.push(value)); - Array.from({ length: 10 }).map((_, i) => mapper(i)); - await queue.waitUntilEmpty(); - const hrend = process.hrtime(hrstart); - expect(hrend[0]).toBe(0); - expect(hrend[1] / 1000000).toBeLessThan(720); - expect(results).toHaveLength(10); - queue.clear(); - }); - }); - describe('#Sad path', () => { - it(`enforce number in options.concurrency`, async () => { - expect(() => new Limiter({ concurrency: 0 })).toThrow(); - expect(() => new Limiter({ concurrency: -1 })).toThrow(); - expect(() => new Limiter({ concurrency: NaN })).toThrow(); - expect(() => new Limiter({ concurrency: Infinity })).not.toThrow(); - expect(() => new Limiter({ concurrency: undefined })).not.toThrow(); - }); - it(`enforce number in options.delay`, async () => { - expect(() => new Limiter({ delay: 0 })).not.toThrow(); - expect(() => new Limiter({ delay: -1 })).toThrow(); - expect(() => new Limiter({ delay: NaN })).toThrow(); - expect(() => new Limiter({ delay: Infinity })).toThrow(); - expect(() => new Limiter({ delay: undefined })).not.toThrow(); - }); - it(`enforce boolean in options.autoStart`, async () => { - expect(() => new Limiter({ autoStart: true })).not.toThrow(); - expect(() => new Limiter({ autoStart: false })).not.toThrow(); - //@ts-expect-error - testing invalid assignment - expect(() => new Limiter({ autoStart: 'd' })).toThrow(); - }); - it(`enforce number in options.highWater`, async () => { - expect(() => new Limiter({ highWater: 1 })).not.toThrow(); - expect(() => new Limiter({ highWater: 0 })).toThrow(); - expect(() => new Limiter({ highWater: NaN })).toThrow(); - expect(() => new Limiter({ highWater: Infinity })).not.toThrow(); - expect(() => new Limiter({ highWater: undefined })).not.toThrow(); - }); - it(`enforce number in options.interval`, async () => { - expect(() => new Limiter({ interval: 0 })).not.toThrow(); - expect(() => new Limiter({ interval: -1 })).toThrow(); - expect(() => new Limiter({ interval: NaN })).toThrow(); - expect(() => new Limiter({ interval: Infinity })).toThrow(); - expect(() => new Limiter({ interval: undefined })).not.toThrow(); - }); - it(`enforce number in options.bucketSize`, async () => { - expect(() => { - const limiter = new Limiter({ bucketSize: 0 }); - limiter.clear(); - }).not.toThrow(); - expect(() => new Limiter({ bucketSize: -1 })).toThrow(); - expect(() => new Limiter({ bucketSize: NaN })).toThrow(); - expect(() => { - const limiter = new Limiter({ bucketSize: Infinity }); - limiter.clear(); - }).not.toThrow(); - expect(() => { - const limiter = new Limiter({ bucketSize: undefined }); - limiter.clear(); - }).not.toThrow(); - }); - it(`enforce number in options.strategy`, async () => { - expect(() => new Limiter({ strategy: STRATEGY.LEAK })).not.toThrow(); - //@ts-expect-error - testing invalid assignment - expect(() => new Limiter({ strategy: -1 })).toThrow(); - //@ts-expect-error - testing invalid assignment - expect(() => new Limiter({ strategy: 'other' })).toThrow(); - expect(() => new Limiter({ strategy: undefined })).not.toThrow(); - }); - it(`enforce number in options.tokensPerInterval`, async () => { - expect(() => new Limiter({ tokensPerInterval: 0 })).not.toThrow(); - expect(() => new Limiter({ tokensPerInterval: -1 })).toThrow(); - expect(() => new Limiter({ tokensPerInterval: NaN })).toThrow(); - expect(() => new Limiter({ tokensPerInterval: Infinity })).toThrow(); - expect(() => new Limiter({ tokensPerInterval: undefined })).not.toThrow(); - }); - it(`enforce number in options.penalty`, async () => { - expect(() => new Limiter({ penalty: 0 })).not.toThrow(); - expect(() => new Limiter({ penalty: -1 })).toThrow(); - expect(() => new Limiter({ penalty: NaN })).toThrow(); - expect(() => new Limiter({ penalty: Infinity })).toThrow(); - expect(() => new Limiter({ penalty: undefined })).not.toThrow(); - }); - it(`enforce coherent options for block strategy`, async () => { - expect(() => new Limiter({ strategy: STRATEGY.BLOCK })).toThrow(); - expect(() => new Limiter({ strategy: STRATEGY.BLOCK, penalty: 0 })).toThrow(); - expect(() => new Limiter({ strategy: STRATEGY.BLOCK, penalty: 150 })).not.toThrow(); - }); - it(`enforce coherent options for bucketSize, tokensPerInterval and interval`, async () => { - expect(() => { - const limiter = new Limiter({ bucketSize: 150 }); - limiter.clear(); - }).not.toThrow(); - expect(() => new Limiter({ bucketSize: 150, tokensPerInterval: 0 })).toThrow(); - expect(() => new Limiter({ bucketSize: 150, tokensPerInterval: 151 })).toThrow(); - expect(() => new Limiter({ bucketSize: 150, interval: 0 })).toThrow(); - expect(() => { - const limiter = new Limiter({ bucketSize: 150, tokensPerInterval: 150, interval: 150 }); - limiter.clear(); - }).not.toThrow(); - }); - it(`Should fail if we try to pass a non valid promise as task`, async () => { - const queue = new Limiter({ concurrency: 1 }); - try { - // @ts-expect-error - testing invalid assignment - await queue.execute(3, [], { id: 'myId1' }); - } catch (error) { - expect(error).toBeDefined(); - } - }); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { randomInt } from 'crypto'; +import { setTimeout as promiseSetTimeout } from 'timers/promises'; +import { Group, MetaData, Sequence, SequencePattern, Single } from '../Tasks'; +import { Limiter } from './Limiter'; +import { STRATEGY } from './types'; + +const fixture = Symbol('fixture'); + +describe('#Limiter', () => { + describe('#Happy path', () => { + beforeEach(() => {}); + afterEach(() => {}); + it(`Simple test for Limiter`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.schedule(async () => results.push(1), [], { id: 'myId1' }); + queue.schedule(async () => results.push(2)); + queue.schedule(async () => results.push(3)); + const test4 = await queue.execute(async () => results.push(4)); + const test5 = await queue.execute(async () => results.push(5)); + expect(test4).toEqual(4); + expect(test5).toEqual(5); + expect(results).toEqual([1, 2, 3, 4, 5]); + expect(queue.options.concurrency).toEqual(1); + }); + it(`Simple test for Limiter, using piped limiter`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const piped = new Limiter({ concurrency: 1 }); + queue.pipe(piped); + const results: number[] = []; + let id1OnQueue = false; + let id4OnQueue = false; + let id1OnPiped = false; + let id4OnPiped = false; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + id1OnQueue = true; + }); + queue.on('myId4', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(4); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + id4OnQueue = true; + }); + piped.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + piped.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + id1OnPiped = true; + }); + piped.on('myId4', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(4); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + id4OnPiped = true; + }); + queue.schedule(async () => results.push(4), [], { id: 'myId4' }); + queue.schedule(async () => results.push(5)); + queue.schedule(async () => results.push(6)); + piped.schedule(async () => results.push(1), [], { id: 'myId1' }); + piped.schedule(async () => results.push(2)); + piped.schedule(async () => results.push(3)); + const test7 = await queue.execute(async () => results.push(7)); + const test8 = await queue.execute(async () => results.push(8)); + expect(test7).toEqual(7); + expect(test8).toEqual(8); + expect(results).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + expect(id1OnQueue).toBeFalsy(); + expect(id4OnQueue).toBeTruthy(); + expect(id1OnPiped).toBeTruthy(); + expect(id4OnPiped).toBeTruthy(); + }); + it(`Simple test for Limiter, using task single handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.schedule(new Single(async () => results.push(1), [], { id: 'myId1' })); + queue.schedule(new Single(async () => results.push(2))); + queue.schedule(new Single(async () => results.push(3))); + const test4 = await queue.execute(new Single(async () => results.push(4))); + const test5 = await queue.execute(new Single(async () => results.push(5))); + expect(test4).toEqual(4); + expect(test5).toEqual(5); + expect(results).toEqual([1, 2, 3, 4, 5]); + }); + it(`Simple test for Limiter, using task groups handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + const scheduleTasks = [ + new Single(async () => results.push(1), [], { id: 'myId1' }), + new Single(async () => results.push(2)), + new Single(async () => results.push(3)), + ]; + const executeTasks = [ + new Single(async () => results.push(4)), + new Single(async () => results.push(5)), + ]; + queue.schedule(new Group(scheduleTasks)); + const test = await queue.execute(new Group(executeTasks)); + expect(test).toEqual([4, 5]); + expect(results).toEqual([1, 2, 3, 4, 5]); + }); + it(`Simple test for Limiter, using task sequence handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + const sequence: SequencePattern = { + pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], + task: new Single(async () => results.push(3)), + post: [new Single(async () => results.push(4))], + finally: [new Single(async () => results.push(5))], + }; + const test = await queue.execute(new Sequence(sequence, { id: 'myId1' })); + expect(test).toEqual(3); + expect(results).toEqual([1, 2, 3, 4, 5]); + }); + it(`Simple test for Limiter with delay`, async () => { + const queue = new Limiter({ concurrency: 1, delay: 100 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId1', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(1); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + const hrstart = process.hrtime(); + queue.schedule(async () => results.push(1), [], { id: 'myId1' }); + queue.schedule(async () => results.push(2)); + queue.schedule(async () => results.push(3)); + await queue.waitUntilEmpty(); + const hrend = process.hrtime(hrstart); + expect(hrend[0]).toBe(0); + const end = () => hrend[1] / 1000000; + expect(end()).toBeGreaterThan(200); + expect(end()).toBeLessThan(220); + expect(results).toEqual([1, 2, 3]); + }); + it(`Simple test for Limiter with delay and concurrency`, async () => { + const queue = new Limiter({ concurrency: 2, delay: 100 }); + const results: number[] = []; + const hrstart = process.hrtime(); + await queue.execute(async () => results.push(1), [], { id: 'myId1' }); + await queue.execute(async () => results.push(2)); + await queue.execute(async () => results.push(3)); + const hrend = process.hrtime(hrstart); + expect(hrend[0]).toBe(0); + const end = () => hrend[1] / 1000000; + expect(end()).toBeGreaterThan(200); + expect(end()).toBeLessThan(220); + expect(results).toEqual([1, 2, 3]); + }); + it('.execute() should resolve', async () => { + const queue = new Limiter(); + const result = await queue.execute(async () => fixture); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect(result).toEqual(fixture); + }); + it('.execute() should reject if promise rejects', async () => { + const queue = new Limiter(); + try { + await queue.execute( + async () => { + throw new Error('test'); + }, + [], + { id: 'myId', retryOptions: { attempts: 1 } } + ); + } catch (error) { + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('test'); + } + }); + it('.execute() should reject if promise rejects, with single task handlers without bind', async () => { + const queue = new Limiter(); + try { + const single = new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId', retryOptions: { attempts: 1 } } + ); + await queue.execute(single); + } catch (error) { + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('test'); + } + }); + it('.execute() should reject if promise rejects, with single task handlers with bind', async () => { + const queue = new Limiter(); + class myClass { + public async myMethod() { + throw new Error('test'); + } + } + const myInstance = new myClass(); + try { + const single = new Single(myInstance.myMethod, { + id: 'myId', + retryOptions: { attempts: 1 }, + bind: myInstance, + }); + await queue.execute(single); + } catch (error) { + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual('Execution error in task [myId]: test'); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual('Too much attempts [1], the promise will not be retried'); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('test'); + } + }); + it('.execute() should reject if promise rejects, with group task handlers', async () => { + const queue = new Limiter(); + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + try { + const group = new Group( + [ + new Single(async () => fixture), + new Single(async () => fixture), + new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId' } + ), + new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId' } + ), + ], + { id: 'myId' } + ); + await queue.execute(group); + } catch (error) { + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + const message = + 'Execution error in task [myId]: CrashError: Execution error in task [myId]: test,\ncaused by InterruptionError: Too much attempts [1], the promise will not be retried,\ncaused by Error: test,\nCrashError: Execution error in task [myId]: test,\ncaused by InterruptionError: Too much attempts [1], the promise will not be retried,\ncaused by Error: test'; + expect((error as Crash).message).toEqual(message); + const cause = (error as Crash).cause as Multi; + expect(cause.message).toEqual(`At least one of the task grouped failed`); + const internalErrors = cause.causes as Crash[]; + expect(internalErrors).toBeDefined(); + expect(internalErrors[0].message).toEqual('Execution error in task [myId]: test'); + expect(internalErrors[1].message).toEqual('Execution error in task [myId]: test'); + } + }); + it(`.execute() should reject if task promise rejects, using task sequence handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + const sequence: SequencePattern = { + pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], + task: new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId', retryOptions: { attempts: 1 } } + ), + post: [new Single(async () => results.push(4))], + finally: [new Single(async () => results.push(5))], + }; + try { + await queue.execute(new Sequence(sequence, { id: 'myId1' })); + } catch (error) { + expect(results).toEqual([1, 2, 5]); + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual( + 'Execution error in task [myId1]: Execution error in task [myId]: test' + ); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual(`Execution error in task [myId]: test`); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual( + 'Too much attempts [1], the promise will not be retried' + ); + } + }); + it(`.execute() should reject if pre task promise rejects, using task sequence handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + const sequence: SequencePattern = { + pre: [ + new Single(async () => results.push(1)), + new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId', retryOptions: { attempts: 1 } } + ), + ], + task: new Single(async () => results.push(2)), + post: [new Single(async () => results.push(3))], + finally: [new Single(async () => results.push(4))], + }; + try { + await queue.execute(new Sequence(sequence, { id: 'myId1' })); + } catch (error) { + expect(results).toEqual([1, 4]); + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual( + 'Execution error in task [myId1]: Error executing the [pre] phase: Execution error in task [myId]: test' + ); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual( + `Error executing the [pre] phase: Execution error in task [myId]: test` + ); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('Execution error in task [myId]: test'); + expect(internalError.cause).toBeDefined(); + expect(internalError.cause?.message).toEqual( + 'Too much attempts [1], the promise will not be retried' + ); + } + }); + it(`.execute() should reject if post task promise rejects, using task sequence handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + const sequence: SequencePattern = { + pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], + task: new Single(async () => results.push(3)), + post: [ + new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId', retryOptions: { attempts: 1 } } + ), + ], + finally: [new Single(async () => results.push(4))], + }; + try { + await queue.execute(new Sequence(sequence, { id: 'myId1' })); + } catch (error) { + expect(results).toEqual([1, 2, 3, 4]); + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toEqual( + 'Execution error in task [myId1]: Error executing the [post] phase: Execution error in task [myId]: test' + ); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual( + `Error executing the [post] phase: Execution error in task [myId]: test` + ); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('Execution error in task [myId]: test'); + expect(internalError.cause).toBeDefined(); + expect(internalError.cause?.message).toEqual( + 'Too much attempts [1], the promise will not be retried' + ); + } + }); + it(`.execute() should reject if final task promise rejects, using task sequence handlers`, async () => { + const queue = new Limiter({ concurrency: 1 }); + const results: number[] = []; + const sequence: SequencePattern = { + pre: [new Single(async () => results.push(1)), new Single(async () => results.push(2))], + task: new Single(async () => results.push(3)), + post: [new Single(async () => results.push(4))], + finally: [ + new Single( + async () => { + throw new Error('test'); + }, + { id: 'myId', retryOptions: { attempts: 1 } } + ), + ], + }; + let test; + try { + test = await queue.execute(new Sequence(sequence, { id: 'myId1' })); + } catch (error) { + expect(test).toEqual(undefined); + expect(results).toEqual([1, 2, 3, 4]); + expect(error).toBeDefined(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect((error as Multi).message).toEqual( + 'Execution error in task [myId1]: Error executing the [finally] phase: Execution error in task [myId]: test' + ); + const cause = (error as Crash).cause as Crash; + expect(cause.message).toEqual( + `Error executing the [finally] phase: Execution error in task [myId]: test` + ); + const internalError = cause.cause as Crash; + expect(internalError).toBeDefined(); + expect(internalError.message).toEqual('Execution error in task [myId]: test'); + } + }); + it(`.execute() should reject if the queue is full`, async () => { + const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW }); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + try { + await queue.execute(async () => fixture); + } catch (error) { + expect(error).toBeDefined(); + expect(queue.size).toBe(2); + expect(queue.pending).toBe(0); + expect((error as Crash).message).toContain('Execution error in task'); + expect((error as Crash).message).toContain('The job could not be scheduled'); + } + }); + it('.schedule() - limited concurrency', async () => { + const queue = new Limiter({ concurrency: 2 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toEqual(fixture); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + results.push(result); + }); + queue.schedule(async () => fixture, [], { id: 'myId' }); + queue.schedule( + async () => { + await promiseSetTimeout(100); + return fixture; + }, + [], + { id: 'myId' } + ); + expect(queue.size).toBe(2); + expect(queue.pending).toBe(0); + queue.schedule(async () => fixture, [], { id: 'myId' }); + expect(queue.size).toBe(3); + expect(queue.pending).toBe(0); + const test = await queue.execute(async () => fixture, [], { id: 'myId' }); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + expect(test).toEqual(fixture); + await queue.waitUntilEmpty(); + expect(results).toEqual([fixture, fixture, fixture, fixture]); + }); + it(`.schedule() - concurrency: 1`, async () => { + const input = [ + [10, 50], + [20, 33], + [30, 17], + ]; + const results: number[] = []; + const hrstart = process.hrtime(); + const queue = new Limiter({ concurrency: 1 }); + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + results.push(result); + }); + const mapper = async ([value, ms]: readonly number[]) => + queue.schedule( + async () => { + await promiseSetTimeout(ms!); + return value!; + }, + [], + { id: 'myId' } + ); + input.map(mapper); + await queue.waitUntilEmpty(); + const hrend = process.hrtime(hrstart); + expect(hrend[0]).toBe(0); + const end = () => hrend[1] / 1000000; + expect(end()).toBeGreaterThan(90); + expect(end()).toBeLessThan(120); + expect(results).toEqual([10, 20, 30]); + }); + it(`.execute() - concurrency: 3`, async () => { + const concurrency = 3; + const queue = new Limiter({ concurrency, delay: 0 }); + let running = 0; + let index = 0; + const results: number[] = []; + const input = Array.from({ length: 100 }) + .fill(0) + .map(() => + queue.execute(async () => { + running++; + expect(running).toBeLessThanOrEqual(concurrency); + expect(queue.pending).toBeLessThanOrEqual(concurrency); + await promiseSetTimeout(randomInt(5, 15)); + results.push(index++); + running--; + }) + ); + await Promise.all(input); + expect(results).toHaveLength(100); + }); + it(`.schedule() - concurrency: 3`, async () => { + const concurrency = 3; + const queue = new Limiter({ concurrency, delay: 0 }); + let running = 0; + let index = 0; + const results: number[] = []; + Array.from({ length: 100 }) + .fill(0) + .map(() => + queue.schedule(async () => { + running++; + expect(running).toBeLessThanOrEqual(concurrency); + expect(queue.pending).toBeLessThanOrEqual(concurrency); + await promiseSetTimeout(randomInt(5, 15)); + results.push(index++); + running--; + }) + ); + await queue.waitUntilEmpty(); + expect(results).toHaveLength(100); + }); + it(`.schedule() - priority`, async () => { + const results: number[] = []; + const queue = new Limiter({ concurrency: 1 }); + queue.schedule(async () => results.push(1), [], { priority: 1, id: 'myId1-1' }); + queue.schedule(async () => results.push(0), [], { priority: 0, id: 'myId0-1' }); + queue.schedule(async () => results.push(1), [], { priority: 1, id: 'myId1-2' }); + queue.schedule(async () => results.push(2), [], { priority: 2, id: 'myId2-1' }); + queue.schedule(async () => results.push(3), [], { priority: 3, id: 'myId3-1' }); + queue.schedule(async () => results.push(0), [], { priority: -1, id: 'myId0-2' }); + await queue.waitUntilEmpty(); + expect(results).toEqual([3, 2, 1, 1, 0, 0]); + }); + it(`.clear()`, async () => { + const queue = new Limiter({ concurrency: 2, autoStart: false }); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + expect(queue.size).toBe(4); + expect(queue.pending).toBe(0); + queue.clear(); + expect(queue.size).toBe(0); + }); + it(`autoStart: false`, async () => { + const queue = new Limiter({ concurrency: 2, autoStart: false }); + + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + queue.schedule(async () => fixture); + expect(queue.size).toBe(4); + expect(queue.pending).toBe(0); + + queue.start(); + await queue.waitUntilEmpty(); + expect(queue.size).toBe(0); + expect(queue.pending).toBe(0); + }); + it(`.stop()`, async () => { + const queue = new Limiter({ concurrency: 2, autoStart: false }); + + queue.schedule(async () => promiseSetTimeout(100)); + queue.schedule(async () => promiseSetTimeout(100)); + queue.schedule(async () => promiseSetTimeout(100)); + queue.schedule(async () => promiseSetTimeout(100)); + queue.schedule(async () => promiseSetTimeout(100)); + expect(queue.size).toBe(5); + expect(queue.pending).toBe(0); + + queue.start(); + expect(queue.size).toBe(3); + expect(queue.pending).toBe(2); + queue.stop(); + await promiseSetTimeout(200); + expect(queue.size).toBe(3); + expect(queue.pending).toBe(0); + + queue.schedule(async () => promiseSetTimeout(100)); + expect(queue.size).toBe(4); + expect(queue.pending).toBe(0); + + queue.start(); + expect(queue.size).toBe(2); + expect(queue.pending).toBe(2); + queue.stop(); + queue.clear(); + expect(queue.size).toBe(0); + }); + it(`Should apply strategy 'block'`, async () => { + let blocked = false; + let unblocked = false; + const queue = new Limiter({ highWater: 2, strategy: STRATEGY.BLOCK, penalty: 100 }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.on('myId', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + results.push(result); + }); + // @ts-expect-error - testing invalid assignment + queue.queue.on('blocked', () => { + // @ts-expect-error - testing invalid assignment + expect(queue.queue.blocked).toBe(true); + blocked = true; + }); + // @ts-expect-error - testing invalid assignment + queue.queue.on('unblocked', () => { + // @ts-expect-error - testing invalid assignment + expect(queue.queue.blocked).toBe(false); + unblocked = true; + }); + queue.schedule(async () => results.push(1), [], { id: 'myId' }); + expect(queue.size).toBe(1); + queue.schedule(async () => results.push(2), [], { id: 'myId' }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(3), [], { id: 'myId' }); + expect(queue.size).toBe(0); + queue.schedule(async () => results.push(4), [], { id: 'myId' }); + expect(queue.size).toBe(0); + expect(blocked).toBe(true); + await promiseSetTimeout(100); + expect(unblocked).toBe(true); + expect(queue.size).toBe(0); + queue.schedule(async () => results.push(1), [], { id: 'myId' }); + expect(queue.size).toBe(1); + }); + it(`Should apply strategy 'leak'`, async () => { + const queue = new Limiter({ highWater: 2, strategy: STRATEGY.LEAK }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.schedule(async () => results.push(1), [], { id: 'myId1' }); + queue.schedule(async () => results.push(2), [], { id: 'myId2' }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(3), [], { id: 'myId3' }); + expect(queue.size).toBe(2); + await queue.waitUntilEmpty(); + expect(queue.size).toBe(0); + expect(results).toEqual([2, 3]); + }); + it(`Should apply strategy 'overflow-priority'`, async () => { + const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW_PRIORITY }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + queue.schedule(async () => results.push(1), [], { id: 'myId1', priority: 9 }); + queue.schedule(async () => results.push(2), [], { id: 'myId2', priority: 1 }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(3), [], { id: 'myId3', priority: 5 }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(4), [], { id: 'myId4', priority: 5 }); + await queue.waitUntilEmpty(); + expect(queue.size).toBe(0); + expect(results).toEqual([1, 3]); + }); + it(`Should apply strategy 'overflow'`, async () => { + const queue = new Limiter({ highWater: 2, strategy: STRATEGY.OVERFLOW }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + if (error) { + expect(error).toBeDefined(); + expect(error.message).toContain('The job could not be scheduled'); + } + }); + queue.schedule(async () => results.push(1), [], { id: 'myId1', priority: 0 }); + queue.schedule(async () => results.push(2), [], { id: 'myId2', priority: 0 }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(3), [], { id: 'myId3', priority: 9 }); + expect(queue.size).toBe(2); + queue.schedule(async () => results.push(4), [], { id: 'myId4', priority: 9 }); + expect(queue.size).toBe(2); + await queue.waitUntilEmpty(); + expect(queue.size).toBe(0); + expect(results).toEqual([1, 2]); + }); + it(`Should use bucketSize, tokensPerInterval and interval to control the jobs schedule`, async () => { + const queue = new Limiter({ + bucketSize: 3, + tokensPerInterval: 1, + interval: 100, + delay: 0, + concurrency: 3, + }); + const results: number[] = []; + queue.on('done', (uuid: string, result: any, meta: MetaData, error?: Crash) => { + expect(uuid).toBeDefined(); + expect(result).toBeDefined(); + expect(meta).toBeDefined(); + expect(error).toBeUndefined(); + }); + const hrstart = process.hrtime(); + const mapper = async (value: number) => queue.schedule(async () => results.push(value)); + Array.from({ length: 10 }).map((_, i) => mapper(i)); + await queue.waitUntilEmpty(); + const hrend = process.hrtime(hrstart); + expect(hrend[0]).toBe(0); + expect(hrend[1] / 1000000).toBeLessThan(720); + expect(results).toHaveLength(10); + queue.clear(); + }); + }); + describe('#Sad path', () => { + it(`enforce number in options.concurrency`, async () => { + expect(() => new Limiter({ concurrency: 0 })).toThrow(); + expect(() => new Limiter({ concurrency: -1 })).toThrow(); + expect(() => new Limiter({ concurrency: NaN })).toThrow(); + expect(() => new Limiter({ concurrency: Infinity })).not.toThrow(); + expect(() => new Limiter({ concurrency: undefined })).not.toThrow(); + }); + it(`enforce number in options.delay`, async () => { + expect(() => new Limiter({ delay: 0 })).not.toThrow(); + expect(() => new Limiter({ delay: -1 })).toThrow(); + expect(() => new Limiter({ delay: NaN })).toThrow(); + expect(() => new Limiter({ delay: Infinity })).toThrow(); + expect(() => new Limiter({ delay: undefined })).not.toThrow(); + }); + it(`enforce boolean in options.autoStart`, async () => { + expect(() => new Limiter({ autoStart: true })).not.toThrow(); + expect(() => new Limiter({ autoStart: false })).not.toThrow(); + //@ts-expect-error - testing invalid assignment + expect(() => new Limiter({ autoStart: 'd' })).toThrow(); + }); + it(`enforce number in options.highWater`, async () => { + expect(() => new Limiter({ highWater: 1 })).not.toThrow(); + expect(() => new Limiter({ highWater: 0 })).toThrow(); + expect(() => new Limiter({ highWater: NaN })).toThrow(); + expect(() => new Limiter({ highWater: Infinity })).not.toThrow(); + expect(() => new Limiter({ highWater: undefined })).not.toThrow(); + }); + it(`enforce number in options.interval`, async () => { + expect(() => new Limiter({ interval: 0 })).not.toThrow(); + expect(() => new Limiter({ interval: -1 })).toThrow(); + expect(() => new Limiter({ interval: NaN })).toThrow(); + expect(() => new Limiter({ interval: Infinity })).toThrow(); + expect(() => new Limiter({ interval: undefined })).not.toThrow(); + }); + it(`enforce number in options.bucketSize`, async () => { + expect(() => { + const limiter = new Limiter({ bucketSize: 0 }); + limiter.clear(); + }).not.toThrow(); + expect(() => new Limiter({ bucketSize: -1 })).toThrow(); + expect(() => new Limiter({ bucketSize: NaN })).toThrow(); + expect(() => { + const limiter = new Limiter({ bucketSize: Infinity }); + limiter.clear(); + }).not.toThrow(); + expect(() => { + const limiter = new Limiter({ bucketSize: undefined }); + limiter.clear(); + }).not.toThrow(); + }); + it(`enforce number in options.strategy`, async () => { + expect(() => new Limiter({ strategy: STRATEGY.LEAK })).not.toThrow(); + //@ts-expect-error - testing invalid assignment + expect(() => new Limiter({ strategy: -1 })).toThrow(); + //@ts-expect-error - testing invalid assignment + expect(() => new Limiter({ strategy: 'other' })).toThrow(); + expect(() => new Limiter({ strategy: undefined })).not.toThrow(); + }); + it(`enforce number in options.tokensPerInterval`, async () => { + expect(() => new Limiter({ tokensPerInterval: 0 })).not.toThrow(); + expect(() => new Limiter({ tokensPerInterval: -1 })).toThrow(); + expect(() => new Limiter({ tokensPerInterval: NaN })).toThrow(); + expect(() => new Limiter({ tokensPerInterval: Infinity })).toThrow(); + expect(() => new Limiter({ tokensPerInterval: undefined })).not.toThrow(); + }); + it(`enforce number in options.penalty`, async () => { + expect(() => new Limiter({ penalty: 0 })).not.toThrow(); + expect(() => new Limiter({ penalty: -1 })).toThrow(); + expect(() => new Limiter({ penalty: NaN })).toThrow(); + expect(() => new Limiter({ penalty: Infinity })).toThrow(); + expect(() => new Limiter({ penalty: undefined })).not.toThrow(); + }); + it(`enforce coherent options for block strategy`, async () => { + expect(() => new Limiter({ strategy: STRATEGY.BLOCK })).toThrow(); + expect(() => new Limiter({ strategy: STRATEGY.BLOCK, penalty: 0 })).toThrow(); + expect(() => new Limiter({ strategy: STRATEGY.BLOCK, penalty: 150 })).not.toThrow(); + }); + it(`enforce coherent options for bucketSize, tokensPerInterval and interval`, async () => { + expect(() => { + const limiter = new Limiter({ bucketSize: 150 }); + limiter.clear(); + }).not.toThrow(); + expect(() => new Limiter({ bucketSize: 150, tokensPerInterval: 0 })).toThrow(); + expect(() => new Limiter({ bucketSize: 150, tokensPerInterval: 151 })).toThrow(); + expect(() => new Limiter({ bucketSize: 150, interval: 0 })).toThrow(); + expect(() => { + const limiter = new Limiter({ bucketSize: 150, tokensPerInterval: 150, interval: 150 }); + limiter.clear(); + }).not.toThrow(); + }); + it(`Should fail if we try to pass a non valid promise as task`, async () => { + const queue = new Limiter({ concurrency: 1 }); + try { + // @ts-expect-error - testing invalid assignment + await queue.execute(3, [], { id: 'myId1' }); + } catch (error) { + expect(error).toBeDefined(); + } + }); + }); +}); diff --git a/packages/api/tasks/src/Limiter/LimiterStateHandler.ts b/packages/api/tasks/src/Limiter/LimiterStateHandler.ts index fa6d31e2..1e020c3f 100644 --- a/packages/api/tasks/src/Limiter/LimiterStateHandler.ts +++ b/packages/api/tasks/src/Limiter/LimiterStateHandler.ts @@ -1,286 +1,300 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import EventEmitter from 'events'; -import { cloneDeep, merge } from 'lodash'; -import { DoneEventHandler, TaskHandler } from '../Tasks'; -import { Queue } from './Queue'; -import { ConsolidatedLimiterOptions, DEFAULT_OPTIONS, LimiterOptions, LimiterState } from './types'; - -export declare interface LimiterStateHandler { - /** - * Register an event listener over the `done` event, which is emitted when a task has ended, either - * due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - on(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Register an event listener over the `done` event, which is emitted when a task has ended, either - * due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - addListener(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Registers a event listener over the `done` event, at the beginning of the listeners array, - * which is emitted when a task has ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - prependListener(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, which is emitted when a task has - * ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - once(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, at the beginning of the listeners - * array, which is emitted when a task has ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - prependOnceListener(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - removeListener(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - off(event: 'done' | string, listener: DoneEventHandler): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `done` event - */ - removeAllListeners(event?: 'done'): this; - /** - * Register an event listener over the `refill` event, which is emitted when queue bucket is refilled - * @param event - `refill` event - * @param listener - The listener function to add - */ - on(event: 'refill', listener: () => void): this; - /** - * Register an event listener over the `refill` event, which is emitted when queue bucket is refilled - * @param event - `refill` event - * @param listener - The listener function to add - */ - addListener(event: 'refill', listener: () => void): this; - /** - * Registers a event listener over the `refill` event, at the beginning of the listeners array, - * which is emitted when queue bucket is refilled - * @param event - `refill` event - * @param listener - The listener function to add - */ - prependListener(event: 'refill', listener: () => void): this; - /** - * Registers a one-time event listener over the `refill` event, which is emitted when queue bucket - * is refilled - * @param event - `refill` event - * @param listener - The listener function to add - */ - once(event: 'refill', listener: () => void): this; - /** - * Registers a one-time event listener over the `refill` event, at the beginning of the listeners - * array, which is emitted when queue bucket is refilled - * @param event - `refill` event - * @param listener - The listener function to add - */ - prependOnceListener(event: 'refill', listener: () => void): this; - /** - * Removes the specified listener from the listener array for the `refill` event. - * @param event - `refill` event - * @param listener - The listener function to remove - */ - removeListener(event: 'refill', listener: () => void): this; - /** - * Removes the specified listener from the listener array for the `refill` event. - * @param event - `refill` event - * @param listener - The listener function to remove - */ - off(event: 'refill', listener: () => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `refill` event - */ - removeAllListeners(event?: 'refill'): this; - /** - * Register an event listener over the `seed` event, which is emitted when queue is empty and a - * new task is added - * @param event - `seed` event - * @param listener - The listener function to add - */ - on(event: 'seed', listener: () => void): this; - /** - * Register an event listener over the `seed` event, which is emitted when queue is empty and a - * new task is added - * @param event - `seed` event - * @param listener - The listener function to add - */ - addListener(event: 'seed', listener: () => void): this; - /** - * Registers a event listener over the `seed` event, at the beginning of the listeners array, which - * is emitted when queue is empty and a new task is added - * @param event - `seed` event - * @param listener - The listener function to add - */ - prependListener(event: 'seed', listener: () => void): this; - /** - * Registers a one-time event listener over the `seed` event, which is emitted when queue is empty - * and a new task is added - * @param event - `seed` event - * @param listener - The listener function to add - */ - once(event: 'seed', listener: () => void): this; - /** - * Registers a one-time event listener over the `seed` event, at the beginning of the listeners - * array, which is emitted when queue is empty and a new task is added - * @param event - `seed` event - * @param listener - The listener function to add - */ - prependOnceListener(event: 'seed', listener: () => void): this; - /** - * Removes the specified listener from the listener array for the `seed` event. - * @param event - `seed` event - * @param listener - The listener function to remove - */ - removeListener(event: 'seed', listener: () => void): this; - /** - * Removes the specified listener from the listener array for the `seed` event. - * @param event - `seed` event - * @param listener - The listener function to remove - */ - off(event: 'seed', listener: () => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `seed` event - */ - removeAllListeners(event?: 'seed'): this; -} -/** - * A limiter state handler is a class that manages the state of a limiter, including the queue of tasks, - * the concurrency, and the limiter options. - */ -export abstract class LimiterStateHandler extends EventEmitter { - /** The limiter queue of tasks */ - private readonly queue: Queue; - /** The limiter options */ - private readonly _options: ConsolidatedLimiterOptions; - /** The limiter state */ - private limiterState: LimiterState = LimiterState.STOPPED; - /** The actual number of concurrent jobs */ - private concurrency: number = 0; - /** - * Create a new instance of Limiter - * @param options - The limiter options - */ - constructor(options?: LimiterOptions) { - super(); - this._options = merge(cloneDeep(DEFAULT_OPTIONS), options); - this.checkOptions(this._options); - this.limiterState = this._options.autoStart ? LimiterState.IDLE : LimiterState.STOPPED; - this.concurrency = 0; - this.queue = new Queue(this._options); - this.queue.on('seed', () => this.emit('seed')); - this.queue.on('refill', () => this.emit('refill')); - } - /** Increase the concurrency */ - protected inc(): void { - this.concurrency++; - } - /** Decrease the concurrency */ - protected dec(): void { - this.concurrency--; - } - /** Enqueue a task */ - protected enqueue(task: TaskHandler): boolean { - return this.queue.enqueue(task); - } - /** Dequeue a task */ - protected dequeue(): TaskHandler | undefined { - return this.queue.dequeue(); - } - /** Stop the limiter */ - protected stop(): void { - this.limiterState = LimiterState.STOPPED; - } - /** Start the limiter */ - protected start(): void { - this.limiterState = LimiterState.STARTING; - } - /** Check if the limiter is at capacity */ - protected get atCapacity(): boolean { - return this.concurrency >= this.options.concurrency; - } - /** Check if the limiter can process more tasks */ - protected get canProcessMore(): boolean { - return !this.atCapacity && this.queue.size > 0 && this.limiterState !== LimiterState.STOPPED; - } - /** Returns the configured delay between tasks */ - protected get delay(): number { - return this._options.delay; - } - /** - * Checks the options for the limiter and throws an error if they are invalid - * @param options - The options to check - */ - private checkOptions(options: LimiterOptions): void { - if ( - typeof options.concurrency !== 'number' || - options.concurrency < 1 || - Number.isNaN(options.concurrency) - ) { - throw new Crash('The concurrency must be at least 1', { name: 'ValidationError' }); - } - if ( - typeof options.delay !== 'number' || - options.delay < 0 || - Number.isNaN(options.delay) || - !Number.isFinite(options.delay) - ) { - throw new Crash('The delay should be a finite number greater than or equal to 0', { - name: 'ValidationError', - }); - } - if (typeof options.autoStart !== 'boolean') { - throw new Crash('The autoStart should be a boolean', { name: 'ValidationError' }); - } - } - /** Returns the number of jobs in the queue */ - public get size(): number { - return this.queue.size; - } - /** Returns the number of pending jobs */ - public get pending(): number { - return this.concurrency; - } - /** Clears the queue */ - public clear(): void { - this.queue.clear(); - } - /** Returns the limiter options */ - public get options(): Readonly { - return this._options; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import EventEmitter from 'events'; +import { cloneDeep, merge } from 'lodash'; +import { DoneEventHandler, TaskHandler } from '../Tasks'; +import { Queue } from './Queue'; +import { ConsolidatedLimiterOptions, DEFAULT_OPTIONS, LimiterOptions, LimiterState } from './types'; + +export declare interface LimiterStateHandler { + /** + * Register an event listener over the `done` event, which is emitted when a task has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - Done event listener + * @event + */ + on(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Register an event listener over the `done` event, which is emitted when a task has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - Done event listener + * @event + */ + addListener(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Registers a event listener over the `done` event, at the beginning of the listeners array, + * which is emitted when a task has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event listener + * @event + */ + prependListener(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, which is emitted when a task has + * ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event listener + * @event + */ + once(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, at the beginning of the listeners + * array, which is emitted when a task has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event listener + * @event + */ + prependOnceListener(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - The listener function to remove + * @event + */ + removeListener(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - The listener function to remove + * @event + */ + off(event: 'done' | string, listener: DoneEventHandler): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `done` event + * @event + */ + removeAllListeners(event?: 'done'): this; + /** + * Register an event listener over the `refill` event, which is emitted when queue bucket is refilled + * @param event - `refill` event + * @param listener - Refill event listener + * @event + */ + on(event: 'refill', listener: () => void): this; + /** + * Register an event listener over the `refill` event, which is emitted when queue bucket is refilled + * @param event - `refill` event + * @param listener - Refill event listener + * @event + */ + addListener(event: 'refill', listener: () => void): this; + /** + * Registers a event listener over the `refill` event, at the beginning of the listeners array, + * which is emitted when queue bucket is refilled + * @param event - `refill` event + * @param listener - Refill event listener + * @event + */ + prependListener(event: 'refill', listener: () => void): this; + /** + * Registers a one-time event listener over the `refill` event, which is emitted when queue bucket + * is refilled + * @param event - `refill` event + * @param listener - Refill event listener + * @event + */ + once(event: 'refill', listener: () => void): this; + /** + * Registers a one-time event listener over the `refill` event, at the beginning of the listeners + * array, which is emitted when queue bucket is refilled + * @param event - `refill` event + * @param listener - Refill event listener + * @event + */ + prependOnceListener(event: 'refill', listener: () => void): this; + /** + * Removes the specified listener from the listener array for the `refill` event. + * @param event - `refill` event + * @param listener - The listener function to remove + * @event + */ + removeListener(event: 'refill', listener: () => void): this; + /** + * Removes the specified listener from the listener array for the `refill` event. + * @param event - `refill` event + * @param listener - The listener function to remove + * @event + */ + off(event: 'refill', listener: () => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `refill` event + * @event + */ + removeAllListeners(event?: 'refill'): this; + /** + * Register an event listener over the `seed` event, which is emitted when queue is empty and a + * new task is added + * @param event - `seed` event + * @param listener - The listener function to add + * @event + */ + on(event: 'seed', listener: () => void): this; + /** + * Register an event listener over the `seed` event, which is emitted when queue is empty and a + * new task is added + * @param event - `seed` event + * @param listener - The listener function to add + * @event + */ + addListener(event: 'seed', listener: () => void): this; + /** + * Registers a event listener over the `seed` event, at the beginning of the listeners array, which + * is emitted when queue is empty and a new task is added + * @param event - `seed` event + * @param listener - The listener function to add + * @event + */ + prependListener(event: 'seed', listener: () => void): this; + /** + * Registers a one-time event listener over the `seed` event, which is emitted when queue is empty + * and a new task is added + * @param event - `seed` event + * @param listener - The listener function to add + * @event + */ + once(event: 'seed', listener: () => void): this; + /** + * Registers a one-time event listener over the `seed` event, at the beginning of the listeners + * array, which is emitted when queue is empty and a new task is added + * @param event - `seed` event + * @param listener - The listener function to add + * @event + */ + prependOnceListener(event: 'seed', listener: () => void): this; + /** + * Removes the specified listener from the listener array for the `seed` event. + * @param event - `seed` event + * @param listener - The listener function to remove + * @event + */ + removeListener(event: 'seed', listener: () => void): this; + /** + * Removes the specified listener from the listener array for the `seed` event. + * @param event - `seed` event + * @param listener - The listener function to remove + * @event + */ + off(event: 'seed', listener: () => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `seed` event + * @event + */ + removeAllListeners(event?: 'seed'): this; +} +/** + * A limiter state handler is a class that manages the state of a limiter, including the queue of tasks, + * the concurrency, and the limiter options. + */ +export abstract class LimiterStateHandler extends EventEmitter { + /** The limiter queue of tasks */ + private readonly queue: Queue; + /** The limiter options */ + private readonly _options: ConsolidatedLimiterOptions; + /** The limiter state */ + private limiterState: LimiterState = LimiterState.STOPPED; + /** The actual number of concurrent jobs */ + private concurrency: number = 0; + /** + * Create a new instance of Limiter + * @param options - The limiter options + */ + constructor(options?: LimiterOptions) { + super(); + this._options = merge(cloneDeep(DEFAULT_OPTIONS), options); + this.checkOptions(this._options); + this.limiterState = this._options.autoStart ? LimiterState.IDLE : LimiterState.STOPPED; + this.concurrency = 0; + this.queue = new Queue(this._options); + this.queue.on('seed', () => this.emit('seed')); + this.queue.on('refill', () => this.emit('refill')); + } + /** Increase the concurrency */ + protected inc(): void { + this.concurrency++; + } + /** Decrease the concurrency */ + protected dec(): void { + this.concurrency--; + } + /** Enqueue a task */ + protected enqueue(task: TaskHandler): boolean { + return this.queue.enqueue(task); + } + /** Dequeue a task */ + protected dequeue(): TaskHandler | undefined { + return this.queue.dequeue(); + } + /** Stop the limiter */ + protected stop(): void { + this.limiterState = LimiterState.STOPPED; + } + /** Start the limiter */ + protected start(): void { + this.limiterState = LimiterState.STARTING; + } + /** Check if the limiter is at capacity */ + protected get atCapacity(): boolean { + return this.concurrency >= this.options.concurrency; + } + /** Check if the limiter can process more tasks */ + protected get canProcessMore(): boolean { + return !this.atCapacity && this.queue.size > 0 && this.limiterState !== LimiterState.STOPPED; + } + /** Returns the configured delay between tasks */ + protected get delay(): number { + return this._options.delay; + } + /** + * Checks the options for the limiter and throws an error if they are invalid + * @param options - The options to check + */ + private checkOptions(options: LimiterOptions): void { + if ( + typeof options.concurrency !== 'number' || + options.concurrency < 1 || + Number.isNaN(options.concurrency) + ) { + throw new Crash('The concurrency must be at least 1', { name: 'ValidationError' }); + } + if ( + typeof options.delay !== 'number' || + options.delay < 0 || + Number.isNaN(options.delay) || + !Number.isFinite(options.delay) + ) { + throw new Crash('The delay should be a finite number greater than or equal to 0', { + name: 'ValidationError', + }); + } + if (typeof options.autoStart !== 'boolean') { + throw new Crash('The autoStart should be a boolean', { name: 'ValidationError' }); + } + } + /** Returns the number of jobs in the queue */ + public get size(): number { + return this.queue.size; + } + /** Returns the number of pending jobs */ + public get pending(): number { + return this.concurrency; + } + /** Clears the queue */ + public clear(): void { + this.queue.clear(); + } + /** Returns the limiter options */ + public get options(): Readonly { + return this._options; + } +} diff --git a/packages/api/tasks/src/Limiter/Queue.ts b/packages/api/tasks/src/Limiter/Queue.ts index ca3f62f6..7404b807 100644 --- a/packages/api/tasks/src/Limiter/Queue.ts +++ b/packages/api/tasks/src/Limiter/Queue.ts @@ -1,328 +1,320 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import { EventEmitter } from 'stream'; -import { TaskHandler } from '../Tasks'; -import { ConsolidatedQueueOptions, STRATEGIES } from './types'; - -export interface Queue extends EventEmitter { - /** Emitted when a job is enqueued */ - on(event: 'enqueue', listener: () => void): this; - /** Emitted when a job is dequeued */ - on(event: 'dequeue', listener: () => void): this; - /** Emitted when a job is removed */ - on(event: 'removed', listener: () => void): this; - /** Emitted when the queue is cleared */ - on(event: 'cleared', listener: () => void): this; - /** Emitted when the queue is blocked */ - on(event: 'blocked', listener: () => void): this; - /** Emitted when the queue is unblocked */ - on(event: 'unblocked', listener: () => void): this; - /** Emitted when the queue is empty */ - on(event: 'empty', listener: () => void): this; - /** Emitted when the bucket is refilled */ - on(event: 'refill', listener: () => void): this; - /** Emitted when the queue is empty and a job is enqueued */ - on(event: 'seed', listener: () => void): this; -} -/** Represents a queue */ -export class Queue extends EventEmitter { - /** Limiter queue of tasks */ - private queue: TaskHandler[]; - /** The interval timer for the rate limiter */ - private refillTimer: NodeJS.Timeout | undefined = undefined; - /** The actual bucket size */ - private actualBucketSize; - /** Whether the queue is blocked */ - private blocked: boolean = false; - /** - * Create a new instance of Queue - * @param options - The queue options - */ - constructor(private readonly options: ConsolidatedQueueOptions) { - super(); - this.checkOptions(options); - this.queue = []; - this.actualBucketSize = this.options.bucketSize; - if (this.actualBucketSize > 0) { - this.refillTimer = setTimeout(this.refillBucket, this.options.interval); - } - this.on('dequeue', () => { - if (this.size === 0) { - this.emit('empty'); - } - }); - this.on('enqueue', () => { - if (this.size === 1) { - this.emit('seed'); - } - }); - } - /** - * Enqueues a job with optional priority - * @param job - The job to enqueue - */ - public enqueue(job: TaskHandler): boolean { - if (!this.handleQueueStrategy(job)) { - return false; - } - if (this.size && this.queue[this.size - 1].priority >= job.priority) { - this.queue.push(job); - this.emit('enqueue'); - return true; - } - const index = this.lowerBound(this.queue, job, (a, b) => b.priority - a.priority); - this.queue.splice(index, 0, job); - this.emit('enqueue'); - return true; - } - /** - * Dequeues and returns the next job in the queue - * @returns The next job, or undefined if the queue is empty - */ - public dequeue(): TaskHandler | undefined { - if (this.checkAndDecrementBucketSize(this.queue[0].weight)) { - const entry = this.queue.shift(); - this.emit('dequeue'); - return entry; - } else { - return undefined; - } - } - /** - * Drops the oldest job in the queue with the lowest priority if not priority is provided, and - * drops the oldest job with a lower priority than the provided priority otherwise - * @remarks - * The queue is ordered by priority and age, this means that first jobs have a higher priority, - * and if several jobs have the same priority, the oldest one its earlier in the queue - * @param priority - The priority of the job to drop - * @returns The dropped job, or undefined if the queue is empty - */ - public dropByPriory(priority?: number): TaskHandler | undefined { - if (this.size === 0) { - return undefined; - } - let _priority: number; - if (priority === undefined) { - // If no priority is provided, drop the oldest job with the lowest priority in the queue - _priority = this.frontPeek()!.priority; - } else { - // If a priority is provided, drop the oldest job with a lower priority than the provided one - _priority = priority - 1; - } - const index = this.queue.findIndex(job => job.priority <= _priority); - // If no job has a lower priority, no job is dropped - if (index === -1) { - return undefined; - } - return this.remove(this.queue[index].uuid); - } - /** - * Peeks the next job in the queue - * @returns The next job, or undefined if the queue is empty - */ - public frontPeek(): TaskHandler | undefined { - return this.queue[0]; - } - /** - * Removes a job from the queue - * @param uuid - The uuid of the job to remove - */ - public remove(uuid: string): TaskHandler | undefined { - const index = this.queue.findIndex(job => job.uuid === uuid); - if (index === -1) { - return undefined; - } - const job = this.queue[index]; - this.queue.splice(index, 1); - this.emit('removed'); - return job; - } - /** Clears the queue */ - public clear(): void { - this.queue = []; - this.emit('cleared'); - if (this.refillTimer) { - clearTimeout(this.refillTimer); - this.refillTimer = undefined; - this.actualBucketSize = this.options.bucketSize; - } - } - /** Gets the size of the queue */ - public get size(): number { - return this.queue.length; - } - /** - * Checks the options for the queue and throws an error if they are invalid - * @param options - The options to check - */ - private checkOptions(options: ConsolidatedQueueOptions): void { - if ( - typeof options.highWater !== 'number' || - options.highWater < 1 || - Number.isNaN(options.highWater) - ) { - throw new Crash('The highWater should be a number greater than 0', { - name: 'ValidationError', - }); - } - if ( - typeof options.bucketSize !== 'number' || - options.bucketSize < 0 || - Number.isNaN(options.bucketSize) - ) { - throw new Crash('The bucketSize should be a number', { name: 'ValidationError' }); - } - if ( - typeof options.interval !== 'number' || - options.interval < 0 || - Number.isNaN(options.interval) || - !Number.isFinite(options.interval) - ) { - throw new Crash('The interval should be a number greater than or equal to 0', { - name: 'ValidationError', - }); - } - if (typeof options.strategy !== 'string' || !STRATEGIES.includes(options.strategy)) { - throw new Crash(`The strategy should be one of ${STRATEGIES.join(', ')}`, { - name: 'ValidationError', - }); - } - if ( - typeof options.tokensPerInterval !== 'number' || - options.tokensPerInterval < 0 || - Number.isNaN(options.tokensPerInterval) || - !Number.isFinite(options.tokensPerInterval) - ) { - throw new Crash('The tokensPerInterval should be a number greater than 0', { - name: 'ValidationError', - }); - } - if ( - typeof options.penalty !== 'number' || - options.penalty < 0 || - Number.isNaN(options.penalty) || - !Number.isFinite(options.penalty) - ) { - throw new Crash('The penalty should be a number', { - name: 'ValidationError', - }); - } - // Check the coherence between the bucket size, the tokens per interval and the interval - if (options.bucketSize > 0) { - if (options.tokensPerInterval > options.bucketSize) { - throw new Crash('The tokensPerInterval should be less than or equal to the bucketSize', { - name: 'ValidationError', - }); - } - if (options.interval <= 0) { - throw new Crash('The interval should be a number greater than 0', { - name: 'ValidationError', - }); - } - if (options.tokensPerInterval <= 0) { - throw new Crash('The tokensPerInterval should be a number greater than 0', { - name: 'ValidationError', - }); - } - } - if (options.strategy === 'block' && options.penalty <= 0) { - throw new Crash('The penalty should be a number greater than 0', { name: 'ValidationError' }); - } - } - /** - * Returns the index of the lower bound - * @param array - The array to search - * @param value - The value to search - * @param comparator - The comparator function - * @returns The index of the lower bound - */ - private lowerBound(array: readonly T[], value: T, comparator: (a: T, b: T) => number): number { - let first = 0; - let count = array.length; - while (count > 0) { - const step = Math.trunc(count / 2); - let it = first + step; - if (comparator(array[it]!, value) <= 0) { - first = ++it; - count -= step + 1; - } else { - count = step; - } - } - return first; - } - /** - * Check if there is enough tokens to execute the task and consume the tokens - * @param weight - task weight - * @returns boolean - */ - private checkAndDecrementBucketSize(weight: number): boolean { - if (this.options.bucketSize <= 0) { - return true; - } - if (this.actualBucketSize < weight) { - return false; - } - this.actualBucketSize -= weight; - return true; - } - /** - * Applies the strategy to the queue - * @param job - The job to add - * @returns True if the job should be added, false otherwise - */ - private handleQueueStrategy(job: TaskHandler): boolean { - if (this.blocked) { - return false; - } - if (this.size < this.options.highWater) { - return true; - } - switch (this.options.strategy) { - // Drop the oldest job with the lowest priority - case 'leak': - this.dropByPriory(); - return true; - // Drop a job that are less important than the one being added - case 'overflow-priority': - if (this.dropByPriory(job.priority)) { - return true; - } - return false; - // Block the queue - case 'block': - this.clear(); - this.blocked = true; - this.emit('blocked'); - setTimeout(() => { - this.blocked = false; - this.emit('unblocked'); - }, this.options.penalty || 0); - return false; - // Do not add the new job - case 'overflow': - default: - return false; - } - } - /** - * Refills the bucket with tokens - * @returns void - */ - private readonly refillBucket = (): void => { - if (this.actualBucketSize < this.options.bucketSize) { - this.actualBucketSize = Math.min( - this.actualBucketSize + this.options.tokensPerInterval, - this.options.bucketSize - ); - this.emit('refill'); - } - this.refillTimer = setTimeout(this.refillBucket, this.options.interval); - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { EventEmitter } from 'stream'; +import { TaskHandler } from '../Tasks'; +import { ConsolidatedQueueOptions, STRATEGIES } from './types'; + +export interface Queue extends EventEmitter { + /** Emitted when a job is enqueued */ + on(event: 'enqueue', listener: () => void): this; + /** Emitted when a job is dequeued */ + on(event: 'dequeue', listener: () => void): this; + /** Emitted when a job is removed */ + on(event: 'removed', listener: () => void): this; + /** Emitted when the queue is cleared */ + on(event: 'cleared', listener: () => void): this; + /** Emitted when the queue is blocked */ + on(event: 'blocked', listener: () => void): this; + /** Emitted when the queue is unblocked */ + on(event: 'unblocked', listener: () => void): this; + /** Emitted when the queue is empty */ + on(event: 'empty', listener: () => void): this; + /** Emitted when the bucket is refilled */ + on(event: 'refill', listener: () => void): this; + /** Emitted when the queue is empty and a job is enqueued */ + on(event: 'seed', listener: () => void): this; +} +/** Represents a queue */ +export class Queue extends EventEmitter { + /** Limiter queue of tasks */ + private queue: TaskHandler[]; + /** The interval timer for the rate limiter */ + private refillTimer: NodeJS.Timeout | undefined = undefined; + /** The actual bucket size */ + private actualBucketSize; + /** Whether the queue is blocked */ + private blocked: boolean = false; + /** + * Create a new instance of Queue + * @param options - The queue options + */ + constructor(private readonly options: ConsolidatedQueueOptions) { + super(); + this.checkOptions(options); + this.queue = []; + this.actualBucketSize = this.options.bucketSize; + if (this.actualBucketSize > 0) { + this.refillTimer = setTimeout(this.refillBucket, this.options.interval); + } + this.on('dequeue', () => { + if (this.size === 0) { + this.emit('empty'); + } + }); + this.on('enqueue', () => { + if (this.size === 1) { + this.emit('seed'); + } + }); + } + /** + * Enqueues a job with optional priority + * @param job - The job to enqueue + */ + public enqueue(job: TaskHandler): boolean { + if (!this.handleQueueStrategy(job)) { + return false; + } + if (this.size && this.queue[this.size - 1].priority >= job.priority) { + this.queue.push(job); + this.emit('enqueue'); + return true; + } + const index = this.lowerBound(this.queue, job, (a, b) => b.priority - a.priority); + this.queue.splice(index, 0, job); + this.emit('enqueue'); + return true; + } + /** + * Dequeues and returns the next job in the queue + * @returns The next job, or undefined if the queue is empty + */ + public dequeue(): TaskHandler | undefined { + if (this.checkAndDecrementBucketSize(this.queue[0].weight)) { + const entry = this.queue.shift(); + this.emit('dequeue'); + return entry; + } else { + return undefined; + } + } + /** + * Drops the oldest job in the queue with the lowest priority if not priority is provided, and + * drops the oldest job with a lower priority than the provided priority otherwise + * @remarks + * The queue is ordered by priority and age, this means that first jobs have a higher priority, + * and if several jobs have the same priority, the oldest one its earlier in the queue + * @param priority - The priority of the job to drop + * @returns The dropped job, or undefined if the queue is empty + */ + public dropByPriory(priority?: number): TaskHandler | undefined { + if (this.size === 0) { + return undefined; + } + let _priority: number; + if (priority === undefined) { + // If no priority is provided, drop the oldest job with the lowest priority in the queue + _priority = this.frontPeek()!.priority; + } else { + // If a priority is provided, drop the oldest job with a lower priority than the provided one + _priority = priority - 1; + } + const index = this.queue.findIndex(job => job.priority <= _priority); + // If no job has a lower priority, no job is dropped + if (index === -1) { + return undefined; + } + return this.remove(this.queue[index].uuid); + } + /** + * Peeks the next job in the queue + * @returns The next job, or undefined if the queue is empty + */ + public frontPeek(): TaskHandler | undefined { + return this.queue[0]; + } + /** + * Removes a job from the queue + * @param uuid - The uuid of the job to remove + */ + public remove(uuid: string): TaskHandler | undefined { + const index = this.queue.findIndex(job => job.uuid === uuid); + if (index === -1) { + return undefined; + } + const job = this.queue[index]; + this.queue.splice(index, 1); + this.emit('removed'); + return job; + } + /** Clears the queue */ + public clear(): void { + this.queue = []; + this.emit('cleared'); + if (this.refillTimer) { + clearTimeout(this.refillTimer); + this.refillTimer = undefined; + this.actualBucketSize = this.options.bucketSize; + } + } + /** Gets the size of the queue */ + public get size(): number { + return this.queue.length; + } + /** + * Checks the options for the queue and throws an error if they are invalid + * @param options - The options to check + */ + private checkOptions(options: ConsolidatedQueueOptions): void { + if (this.isLessThan(options.highWater, 1, true)) { + throw new Crash('The highWater should be a number greater than 0', { + name: 'ValidationError', + }); + } + if (this.isLessThan(options.bucketSize, 0, true)) { + throw new Crash('The bucketSize should be a number', { name: 'ValidationError' }); + } + if (this.isLessThan(options.interval)) { + throw new Crash('The interval should be a number greater than or equal to 0', { + name: 'ValidationError', + }); + } + if (this.isLessThan(options.tokensPerInterval)) { + throw new Crash('The tokensPerInterval should be a number greater than 0', { + name: 'ValidationError', + }); + } + if (this.isLessThan(options.penalty)) { + throw new Crash('The penalty should be a number', { + name: 'ValidationError', + }); + } + if (typeof options.strategy !== 'string' || !STRATEGIES.includes(options.strategy)) { + throw new Crash(`The strategy should be one of ${STRATEGIES.join(', ')}`, { + name: 'ValidationError', + }); + } + // Check the coherence between the bucket size, the tokens per interval and the interval + if (options.bucketSize > 0) { + if (options.tokensPerInterval > options.bucketSize) { + throw new Crash('The tokensPerInterval should be less than or equal to the bucketSize', { + name: 'ValidationError', + }); + } + if (options.interval <= 0) { + throw new Crash('The interval should be a number greater than 0', { + name: 'ValidationError', + }); + } + if (options.tokensPerInterval <= 0) { + throw new Crash('The tokensPerInterval should be a number greater than 0', { + name: 'ValidationError', + }); + } + } + if (options.strategy === 'block' && options.penalty <= 0) { + throw new Crash('The penalty should be a number greater than 0', { name: 'ValidationError' }); + } + } + /** + * Returns the index of the lower bound + * @param array - The array to search + * @param value - The value to search + * @param comparator - The comparator function + * @returns The index of the lower bound + */ + private lowerBound(array: readonly T[], value: T, comparator: (a: T, b: T) => number): number { + let first = 0; + let count = array.length; + while (count > 0) { + const step = Math.trunc(count / 2); + let it = first + step; + if (comparator(array[it], value) <= 0) { + first = ++it; + count -= step + 1; + } else { + count = step; + } + } + return first; + } + /** + * Check if there is enough tokens to execute the task and consume the tokens + * @param weight - task weight + * @returns boolean + */ + private checkAndDecrementBucketSize(weight: number): boolean { + if (this.options.bucketSize <= 0) { + return true; + } + if (this.actualBucketSize < weight) { + return false; + } + this.actualBucketSize -= weight; + return true; + } + /** + * Applies the strategy to the queue + * @param job - The job to add + * @returns True if the job should be added, false otherwise + */ + private handleQueueStrategy(job: TaskHandler): boolean { + if (this.blocked) { + return false; + } + if (this.size < this.options.highWater) { + return true; + } + switch (this.options.strategy) { + // Drop the oldest job with the lowest priority + case 'leak': + this.dropByPriory(); + return true; + // Drop a job that are less important than the one being added + case 'overflow-priority': + if (this.dropByPriory(job.priority)) { + return true; + } + return false; + // Block the queue + case 'block': + this.clear(); + this.blocked = true; + this.emit('blocked'); + setTimeout(() => { + this.blocked = false; + this.emit('unblocked'); + }, this.options.penalty || 0); + return false; + // Do not add the new job + case 'overflow': + default: + return false; + } + } + /** + * Refills the bucket with tokens + * @returns void + */ + private readonly refillBucket = (): void => { + if (this.actualBucketSize < this.options.bucketSize) { + this.actualBucketSize = Math.min( + this.actualBucketSize + this.options.tokensPerInterval, + this.options.bucketSize + ); + this.emit('refill'); + } + this.refillTimer = setTimeout(this.refillBucket, this.options.interval); + }; + /** + * Checks if a given value is less than a specified minimum value. + * @param value - The value to be checked. + * @param min - The minimum value to compare against. Defaults to 0. + * @param allowInfinity - Whether to allow infinite values. Defaults to true. + * @returns `true` if the value is less than the minimum value, or if the value is not a number, or if the value is NaN, or if infinite values are not allowed and the value is infinite. Otherwise, returns `false`. + */ + private isLessThan(value: number, min: number = 0, allowInfinity = false): boolean { + return ( + typeof value !== 'number' || + Number.isNaN(value) || + value < min || + (!allowInfinity && !Number.isFinite(value)) + ); + } +} diff --git a/packages/api/tasks/src/Limiter/types/LimiterOptions.i.ts b/packages/api/tasks/src/Limiter/types/LimiterOptions.i.ts index 0a9b0fda..e299dcf8 100644 --- a/packages/api/tasks/src/Limiter/types/LimiterOptions.i.ts +++ b/packages/api/tasks/src/Limiter/types/LimiterOptions.i.ts @@ -1,42 +1,43 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { RetryOptions } from '@mdf.js/utils'; -import { QueueOptions } from './QueueOptions.i'; - -/** Represents the limiter options */ -export interface LimiterOptions extends QueueOptions { - /** - * The maximum number of concurrent jobs - * @default 1 - */ - concurrency?: number; - /** - * Delay between each job in milliseconds - * @default 0 - * For `concurrency = 1`, the delay is applied after each job is finished - * For `concurrency > 1`, if the actual number of concurrent jobs is less than `concurrency`, the - * delay is applied after each job is finished, otherwise, the delay is applied after each job is - * started. - */ - delay?: number; - /** - * Set the default options for the retry process of the jobs - * @default undefined - */ - retryOptions?: RetryOptions; - /** - * Set whether the limiter should start to process the jobs automatically - * @default true - */ - autoStart?: boolean; -} - -/** Represents the consolidated limiter options */ -export interface ConsolidatedLimiterOptions extends Omit, 'retryOptions'> { - retryOptions?: RetryOptions; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { RetryOptions } from '@mdf.js/utils'; +import { QueueOptions } from './QueueOptions.i'; + +/** Represents the limiter options */ +export interface LimiterOptions extends QueueOptions { + /** + * The maximum number of concurrent jobs + * @defaultValue 1 + */ + concurrency?: number; + /** + * Delay between each job in milliseconds + * @defaultValue 0 + * For `concurrency = 1`, the delay is applied after each job is finished + * For `concurrency > 1`, if the actual number of concurrent jobs is less than `concurrency`, the + * delay is applied after each job is finished, otherwise, the delay is applied after each job is + * started. + */ + delay?: number; + /** + * Set the default options for the retry process of the jobs + * @defaultValue undefined + */ + retryOptions?: RetryOptions; + /** + * Set whether the limiter should start to process the jobs automatically + * @defaultValue true + */ + autoStart?: boolean; +} + +/** Represents the consolidated limiter options */ +export interface ConsolidatedLimiterOptions extends Omit, 'retryOptions'> { + retryOptions?: RetryOptions; +} + diff --git a/packages/api/tasks/src/Limiter/types/QueueOptions.i.ts b/packages/api/tasks/src/Limiter/types/QueueOptions.i.ts index cbc54f9f..e7ab227e 100644 --- a/packages/api/tasks/src/Limiter/types/QueueOptions.i.ts +++ b/packages/api/tasks/src/Limiter/types/QueueOptions.i.ts @@ -1,51 +1,52 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Strategy } from './Strategy.t'; - -/** Represents the queue options */ -export interface QueueOptions { - /** - * The maximum number of jobs in the queue - * @default Infinity - */ - highWater?: number; - /** - * The strategy to use when the queue length reaches highWater - * @default 'leak' - */ - strategy?: Strategy; - /** - * The penalty for the BLOCK strategy in milliseconds - * @default 0 - */ - penalty?: number; - /** - * Set the bucket size for the rate limiter - * @default 0 - * If the bucket size is 0, only `concurrency` and `delay` will be used to limit the rate of the - * jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to - * limit the rate of the jobs. The bucket size is the maximum number of tokens that can be - * consumed in the interval. The interval is defined by the `tokensPerInterval` and `interval` - * properties. - * @see https://en.wikipedia.org/wiki/Token_bucket - */ - bucketSize?: number; - /** - * Define the number of tokens that will be added to the bucket at the beginning of the interval - * @default 1 - */ - tokensPerInterval?: number; - /** - * Define the interval in milliseconds - * @default 1000 - */ - interval?: number; -} - -/** Represents the consolidated limiter options */ -export interface ConsolidatedQueueOptions extends Required {} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Strategy } from './Strategy.t'; + +/** Represents the queue options */ +export interface QueueOptions { + /** + * The maximum number of jobs in the queue + * @defaultValue Infinity + */ + highWater?: number; + /** + * The strategy to use when the queue length reaches highWater + * @defaultValue 'leak' + */ + strategy?: Strategy; + /** + * The penalty for the BLOCK strategy in milliseconds + * @defaultValue 0 + */ + penalty?: number; + /** + * Set the bucket size for the rate limiter + * @defaultValue 0 + * If the bucket size is 0, only `concurrency` and `delay` will be used to limit the rate of the + * jobs. If the bucket size is greater than 0, the consumption of the tokens will be used to + * limit the rate of the jobs. The bucket size is the maximum number of tokens that can be + * consumed in the interval. The interval is defined by the `tokensPerInterval` and `interval` + * properties. + * @see https://en.wikipedia.org/wiki/Token_bucket + */ + bucketSize?: number; + /** + * Define the number of tokens that will be added to the bucket at the beginning of the interval + * @defaultValue 1 + */ + tokensPerInterval?: number; + /** + * Define the interval in milliseconds + * @defaultValue 1000 + */ + interval?: number; +} + +/** Represents the consolidated limiter options */ +export interface ConsolidatedQueueOptions extends Required {} + diff --git a/packages/api/tasks/src/Polling/PollingExecutor.ts b/packages/api/tasks/src/Polling/PollingExecutor.ts index 53020bda..a706c9b5 100644 --- a/packages/api/tasks/src/Polling/PollingExecutor.ts +++ b/packages/api/tasks/src/Polling/PollingExecutor.ts @@ -34,7 +34,7 @@ export class PollingExecutor extends EventEmitter { /** Scan timer */ private scanTimer: NodeJS.Timeout | undefined; /** Polling period in ms */ - private pollingPeriod: number; + private readonly pollingPeriod: number; /** Polling manager stats */ private readonly manager: PollingManager; /** Cycle timer */ @@ -64,7 +64,7 @@ export class PollingExecutor extends EventEmitter { this.pollingPeriod = ms(this.options.pollingGroup); } /** Include all the task entries in the limiter */ - private performScan = (): void => { + private readonly performScan = (): void => { this.logger.debug(`Starting polling group ${this.options.pollingGroup}`); if (!this.cycleTimer) { this.cycleTimer = process.hrtime(); @@ -87,7 +87,7 @@ export class PollingExecutor extends EventEmitter { * Event handler for the end of a cycle event * @param check - Health check */ - private onEndCycle = (check: Health.Check): void => { + private readonly onEndCycle = (check: Health.Check): void => { this.logger.debug(`Polling group ${this.options.pollingGroup} ended`); this.logger.debug( `Polling group ${this.options.pollingGroup} stats: ${JSON.stringify(check, null, 2)}` @@ -100,7 +100,7 @@ export class PollingExecutor extends EventEmitter { * Event handler for error events * @param error - Error event */ - private onError = (error: Crash | Multi): void => { + private readonly onError = (error: Crash | Multi): void => { if (this.listenerCount('error') > 0) { this.emit('error', error); } @@ -112,7 +112,12 @@ export class PollingExecutor extends EventEmitter { * @param meta - Task metadata * @param error - Task error */ - private onDone = (uuid: string, result: any, meta: MetaData, error?: Crash | Multi): void => { + private readonly onDone = ( + uuid: string, + result: any, + meta: MetaData, + error?: Crash | Multi + ): void => { this.emit('done', uuid, result, meta, error); }; /** Attach the event listeners */ @@ -148,4 +153,3 @@ export class PollingExecutor extends EventEmitter { return this.manager.check; } } - diff --git a/packages/api/tasks/src/Polling/PollingManager.ts b/packages/api/tasks/src/Polling/PollingManager.ts index 1d06145f..ef29f1c1 100644 --- a/packages/api/tasks/src/Polling/PollingManager.ts +++ b/packages/api/tasks/src/Polling/PollingManager.ts @@ -58,7 +58,7 @@ export class PollingManager extends EventEmitter { /** Fast cycle ratio counter */ private fastCycleRatioCounter = 0; /** Ratio of fast cycles to slow cycles */ - private factCycleToSlowCycleRatio; + private readonly factCycleToSlowCycleRatio: number; /** * Create a polling manager * @param options - Polling manager options @@ -161,7 +161,9 @@ export class PollingManager extends EventEmitter { * @param config - Task configuration * @returns A task handler instance */ - private wrappedCreatedTaskInstance = (config: TaskBaseConfig): TaskHandler | undefined => { + private readonly wrappedCreatedTaskInstance = ( + config: TaskBaseConfig + ): TaskHandler | undefined => { try { return this.createTaskInstance(config); } catch (error) { @@ -175,7 +177,7 @@ export class PollingManager extends EventEmitter { * @param meta - Task metadata * @param error - Task error */ - private onDoneTaskHandler = ( + private readonly onDoneTaskHandler = ( uuid: string, result: any, meta: MetaData, diff --git a/packages/api/tasks/src/Polling/PollingStatsManager.ts b/packages/api/tasks/src/Polling/PollingStatsManager.ts index 5d738eb1..79b0df5b 100644 --- a/packages/api/tasks/src/Polling/PollingStatsManager.ts +++ b/packages/api/tasks/src/Polling/PollingStatsManager.ts @@ -19,7 +19,7 @@ import { export class PollingMetricsHandler { /** Polling stats */ - private pollingStats: PollingStats = { ...DEFAULT_POLLING_STATS }; + private readonly pollingStats: PollingStats = { ...DEFAULT_POLLING_STATS }; /** Scan cycles duration in milliseconds */ private readonly scanCyclesDuration: number[] = []; /** Metrics labels */ @@ -32,7 +32,6 @@ export class PollingMetricsHandler { * @param resource - Resource identifier * @param pollingGroup - Polling group assigned to this manager * @param cyclesOnStats - Number of cycles on stats - * @param logger - Logger instance * @param metrics - Metrics instances */ constructor( @@ -155,4 +154,3 @@ export class PollingMetricsHandler { return _check; } } - diff --git a/packages/api/tasks/src/Polling/RetryManager.ts b/packages/api/tasks/src/Polling/RetryManager.ts index b866a7be..a603941a 100644 --- a/packages/api/tasks/src/Polling/RetryManager.ts +++ b/packages/api/tasks/src/Polling/RetryManager.ts @@ -1,82 +1,81 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { LoggerInstance } from '@mdf.js/logger'; -import { RetryOptions } from '@mdf.js/utils'; -import ms from 'ms'; -import { DEFAULT_MAX_RETRY_FACTOR, DEFAULT_MAX_TIMEOUT } from '.'; -import { WellIdentifiedTaskOptions } from './types'; - -export class RetryManager { - /** Limiter delay */ - private readonly limiterDelay: number; - /** Polling group */ - private readonly pollingGroup: string; - /** Logger */ - private readonly logger: LoggerInstance; - /** - * Create a retry manager - * @param limiterDelay - Limiter delay - * @param pollingGroup - Polling group - * @param logger - Logger - */ - constructor(limiterDelay: number, pollingGroup: string, logger: LoggerInstance) { - this.limiterDelay = limiterDelay; - this.pollingGroup = pollingGroup; - this.logger = logger; - } - /** - * Fast cycle retry options - * @param options - Task options - * @returns Task options with fast cycle retry options - */ - public fastCycleRetryOptions(options: WellIdentifiedTaskOptions): WellIdentifiedTaskOptions { - const attempts = Math.min(DEFAULT_MAX_RETRY_FACTOR, options.retryOptions?.attempts || 1); - return this.cycleRetryOptions(options, attempts); - } - /** - * Slow cycle retry options - * @param options - Task options - * @returns Task options with slow cycle retry options - */ - public slowCycleRetryOptions(options: WellIdentifiedTaskOptions): WellIdentifiedTaskOptions { - return this.cycleRetryOptions(options, 1); - } - /** - * Cycle retry options - * @param options - Task options - * @param attempts - Number of attempts - * @returns Task options with cycle retry options - */ - private cycleRetryOptions( - options: WellIdentifiedTaskOptions, - attempts: number - ): WellIdentifiedTaskOptions { - const timeout = Math.min( - options.retryOptions?.timeout || DEFAULT_MAX_TIMEOUT, - ms(this.pollingGroup) * attempts, - DEFAULT_MAX_TIMEOUT - ); - const waitTime = Math.min( - options.retryOptions?.waitTime || this.limiterDelay, - this.limiterDelay - ); - const retryOptions: RetryOptions = { - logger: this.logger.crash, - attempts, - timeout, - waitTime, - maxWaitTime: waitTime, - interrupt: options.retryOptions?.interrupt, - abortSignal: options.retryOptions?.abortSignal, - }; - return { - ...options, - retryOptions, - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { LoggerInstance } from '@mdf.js/logger'; +import { RetryOptions } from '@mdf.js/utils'; +import ms from 'ms'; +import { DEFAULT_MAX_RETRY_FACTOR, DEFAULT_MAX_TIMEOUT } from '.'; +import { WellIdentifiedTaskOptions } from './types'; + +export class RetryManager { + /** Limiter delay */ + private readonly limiterDelay: number; + /** Polling group */ + private readonly pollingGroup: string; + /** Logger */ + private readonly logger: LoggerInstance; + /** + * Create a retry manager + * @param limiterDelay - Limiter delay + * @param pollingGroup - Polling group + * @param logger - Logger + */ + constructor(limiterDelay: number, pollingGroup: string, logger: LoggerInstance) { + this.limiterDelay = limiterDelay; + this.pollingGroup = pollingGroup; + this.logger = logger; + } + /** + * Fast cycle retry options + * @param options - Task options + * @returns Task options with fast cycle retry options + */ + public fastCycleRetryOptions(options: WellIdentifiedTaskOptions): WellIdentifiedTaskOptions { + const attempts = Math.min(DEFAULT_MAX_RETRY_FACTOR, options.retryOptions?.attempts ?? 1); + return this.cycleRetryOptions(options, attempts); + } + /** + * Slow cycle retry options + * @param options - Task options + * @returns Task options with slow cycle retry options + */ + public slowCycleRetryOptions(options: WellIdentifiedTaskOptions): WellIdentifiedTaskOptions { + return this.cycleRetryOptions(options, 1); + } + /** + * Cycle retry options + * @param options - Task options + * @param attempts - Number of attempts + * @returns Task options with cycle retry options + */ + private cycleRetryOptions( + options: WellIdentifiedTaskOptions, + attempts: number + ): WellIdentifiedTaskOptions { + const timeout = Math.min( + options.retryOptions?.timeout ?? DEFAULT_MAX_TIMEOUT, + ms(this.pollingGroup) * attempts + ); + const waitTime = Math.min( + options.retryOptions?.waitTime ?? this.limiterDelay, + this.limiterDelay + ); + const retryOptions: RetryOptions = { + logger: this.logger.crash, + attempts, + timeout, + waitTime, + maxWaitTime: waitTime, + interrupt: options.retryOptions?.interrupt, + abortSignal: options.retryOptions?.abortSignal, + }; + return { + ...options, + retryOptions, + }; + } +} diff --git a/packages/api/tasks/src/Polling/types/MetricsDefinitions.t.ts b/packages/api/tasks/src/Polling/types/MetricsDefinitions.t.ts index 64fa1a73..63b75de2 100644 --- a/packages/api/tasks/src/Polling/types/MetricsDefinitions.t.ts +++ b/packages/api/tasks/src/Polling/types/MetricsDefinitions.t.ts @@ -1,180 +1,181 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Counter, Gauge, Histogram, Registry } from 'prom-client'; - -/** Metrics for tasks */ -type TaskMetricsDefinitions = { - /** Duration in millisecond for each concrete task, providing insights into task efficiency. */ - task_duration_milliseconds: Histogram; - /** Cumulative count of task performed, for tracking demand and usage patterns. */ - task_total: Counter; - /** Cumulative count of task errors, providing insights into system reliability */ - task_errors_total: Counter; - /** Number of task in progress */ - task_in_progress: Gauge; -}; -/** Metrics for tasks */ -const TASK_METRICS_DEFINITIONS = (registry: Registry): TaskMetricsDefinitions => { - const task_duration_milliseconds = new Histogram({ - name: `task_duration_milliseconds`, - help: `Duration in millisecond for each concrete task, providing insights into task efficiency.`, - labelNames: ['resource', 'taskId'], - buckets: [ - 0.1, 0.5, 1, 2.5, 5, 10, 30, 60, 300, 600, 900, 1800, 3600, 14400, 21600, 43200, 86400, - ], - registers: [registry], - }); - const task_total = new Counter({ - name: `task_total`, - help: `Cumulative count of task performed for tracking demand and usage patterns.`, - labelNames: ['resource', 'taskId'], - registers: [registry], - }); - const task_errors_total = new Counter({ - name: `task_errors_total`, - help: `Cumulative count of task errors, providing insights into system reliability`, - labelNames: ['resource', 'taskId'], - registers: [registry], - }); - const task_in_progress = new Gauge({ - name: `task_in_progress`, - help: `Number of task in progress`, - labelNames: ['resource', 'taskId'], - registers: [registry], - }); - - return { - task_duration_milliseconds, - task_total, - task_errors_total, - task_in_progress, - }; -}; - -/** Metrics for Scan */ -type ScanMetricsDefinitions = { - /** Cumulative count of scan cycles performed. */ - scan_cycles_total: Counter; - /** Cumulative count of scan overruns */ - scan_overruns_total: Counter; - /** Consecutive count of scan overruns */ - scan_overruns_consecutive: Gauge; - /** Total number of task included in the scan */ - scan_task_total: Gauge; - /** Total number of task off scan */ - scan_task_off_scan_total: Gauge; - /** Duration of scan cycle in milliseconds */ - scan_cycle_duration_milliseconds: Histogram; - /** Number of cycles used to generate the stats */ - scan_cycles_on_stats: Gauge; - /** Average duration of scan cycles in milliseconds */ - scan_duration_avg_milliseconds: Gauge; - /** Maximum duration of scan cycles in milliseconds */ - scan_duration_max_milliseconds: Gauge; - /** Minimum duration of scan cycles in milliseconds */ - scan_duration_min_milliseconds: Gauge; - /** Maximum number of task in the scan queue */ - scan_queue_size_max_allowed: Gauge; -}; - -/** Metrics for Scan */ -const SCAN_METRICS_DEFINITIONS = (registry: Registry): ScanMetricsDefinitions => { - const scan_cycles_total = new Counter({ - name: `scan_cycles_total`, - help: `Cumulative count of scan cycles performed.`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_overruns_total = new Counter({ - name: `scan_overruns_total`, - help: `Cumulative count of scan overruns`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_overruns_consecutive = new Gauge({ - name: `scan_overruns_consecutive`, - help: `Consecutive count of scan overruns`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_task_total = new Gauge({ - name: `scan_task_total`, - help: `Total number of task included in the scan`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_task_off_scan_total = new Gauge({ - name: `scan_task_off_scan_total`, - help: `Total number of task off scan`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_cycle_duration_milliseconds = new Histogram({ - name: `scan_cycle_duration_milliseconds`, - help: `Duration of scan cycle in milliseconds`, - labelNames: ['resource', 'pollingGroup'], - buckets: [ - 0.1, 0.5, 1, 2.5, 5, 10, 30, 60, 300, 600, 900, 1800, 3600, 14400, 21600, 43200, 86400, - ], - registers: [registry], - }); - const scan_cycles_on_stats = new Gauge({ - name: `scan_cycles_on_stats`, - help: `Number of cycles used to generate the stats`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_duration_avg_milliseconds = new Gauge({ - name: `scan_duration_avg_milliseconds`, - help: `Average duration of scan cycles in milliseconds`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_duration_max_milliseconds = new Gauge({ - name: `scan_duration_max_milliseconds`, - help: `Maximum duration of scan cycles in milliseconds`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_duration_min_milliseconds = new Gauge({ - name: `scan_duration_min_milliseconds`, - help: `Minimum duration of scan cycles in milliseconds`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - const scan_queue_size_max_allowed = new Gauge({ - name: `scan_queue_size_max_allowed`, - help: `Maximum number of task in the scan queue`, - labelNames: ['resource', 'pollingGroup'], - registers: [registry], - }); - - return { - scan_cycles_total, - scan_overruns_total, - scan_overruns_consecutive, - scan_task_total, - scan_task_off_scan_total, - scan_cycle_duration_milliseconds, - scan_cycles_on_stats, - scan_duration_avg_milliseconds, - scan_duration_max_milliseconds, - scan_duration_min_milliseconds, - scan_queue_size_max_allowed, - }; -}; - -/** Metrics definitions */ -export type MetricsDefinitions = TaskMetricsDefinitions & ScanMetricsDefinitions; - -/** Metrics definitions */ -export const METRICS_DEFINITIONS = (registry: Registry): MetricsDefinitions => { - return { - ...TASK_METRICS_DEFINITIONS(registry), - ...SCAN_METRICS_DEFINITIONS(registry), - }; -}; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Counter, Gauge, Histogram, Registry } from 'prom-client'; + +/** Metrics for tasks */ +export type TaskMetricsDefinitions = { + /** Duration in millisecond for each concrete task, providing insights into task efficiency. */ + task_duration_milliseconds: Histogram; + /** Cumulative count of task performed, for tracking demand and usage patterns. */ + task_total: Counter; + /** Cumulative count of task errors, providing insights into system reliability */ + task_errors_total: Counter; + /** Number of task in progress */ + task_in_progress: Gauge; +}; + +/** Metrics for tasks */ +const TASK_METRICS_DEFINITIONS = (registry: Registry): TaskMetricsDefinitions => { + const task_duration_milliseconds = new Histogram({ + name: `task_duration_milliseconds`, + help: `Duration in millisecond for each concrete task, providing insights into task efficiency.`, + labelNames: ['resource', 'taskId'], + buckets: [ + 0.1, 0.5, 1, 2.5, 5, 10, 30, 60, 300, 600, 900, 1800, 3600, 14400, 21600, 43200, 86400, + ], + registers: [registry], + }); + const task_total = new Counter({ + name: `task_total`, + help: `Cumulative count of task performed for tracking demand and usage patterns.`, + labelNames: ['resource', 'taskId'], + registers: [registry], + }); + const task_errors_total = new Counter({ + name: `task_errors_total`, + help: `Cumulative count of task errors, providing insights into system reliability`, + labelNames: ['resource', 'taskId'], + registers: [registry], + }); + const task_in_progress = new Gauge({ + name: `task_in_progress`, + help: `Number of task in progress`, + labelNames: ['resource', 'taskId'], + registers: [registry], + }); + + return { + task_duration_milliseconds, + task_total, + task_errors_total, + task_in_progress, + }; +}; + +/** Metrics for Scan */ +export type ScanMetricsDefinitions = { + /** Cumulative count of scan cycles performed. */ + scan_cycles_total: Counter; + /** Cumulative count of scan overruns */ + scan_overruns_total: Counter; + /** Consecutive count of scan overruns */ + scan_overruns_consecutive: Gauge; + /** Total number of task included in the scan */ + scan_task_total: Gauge; + /** Total number of task off scan */ + scan_task_off_scan_total: Gauge; + /** Duration of scan cycle in milliseconds */ + scan_cycle_duration_milliseconds: Histogram; + /** Number of cycles used to generate the stats */ + scan_cycles_on_stats: Gauge; + /** Average duration of scan cycles in milliseconds */ + scan_duration_avg_milliseconds: Gauge; + /** Maximum duration of scan cycles in milliseconds */ + scan_duration_max_milliseconds: Gauge; + /** Minimum duration of scan cycles in milliseconds */ + scan_duration_min_milliseconds: Gauge; + /** Maximum number of task in the scan queue */ + scan_queue_size_max_allowed: Gauge; +}; + +/** Metrics for Scan */ +const SCAN_METRICS_DEFINITIONS = (registry: Registry): ScanMetricsDefinitions => { + const scan_cycles_total = new Counter({ + name: `scan_cycles_total`, + help: `Cumulative count of scan cycles performed.`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_overruns_total = new Counter({ + name: `scan_overruns_total`, + help: `Cumulative count of scan overruns`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_overruns_consecutive = new Gauge({ + name: `scan_overruns_consecutive`, + help: `Consecutive count of scan overruns`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_task_total = new Gauge({ + name: `scan_task_total`, + help: `Total number of task included in the scan`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_task_off_scan_total = new Gauge({ + name: `scan_task_off_scan_total`, + help: `Total number of task off scan`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_cycle_duration_milliseconds = new Histogram({ + name: `scan_cycle_duration_milliseconds`, + help: `Duration of scan cycle in milliseconds`, + labelNames: ['resource', 'pollingGroup'], + buckets: [ + 0.1, 0.5, 1, 2.5, 5, 10, 30, 60, 300, 600, 900, 1800, 3600, 14400, 21600, 43200, 86400, + ], + registers: [registry], + }); + const scan_cycles_on_stats = new Gauge({ + name: `scan_cycles_on_stats`, + help: `Number of cycles used to generate the stats`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_duration_avg_milliseconds = new Gauge({ + name: `scan_duration_avg_milliseconds`, + help: `Average duration of scan cycles in milliseconds`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_duration_max_milliseconds = new Gauge({ + name: `scan_duration_max_milliseconds`, + help: `Maximum duration of scan cycles in milliseconds`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_duration_min_milliseconds = new Gauge({ + name: `scan_duration_min_milliseconds`, + help: `Minimum duration of scan cycles in milliseconds`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + const scan_queue_size_max_allowed = new Gauge({ + name: `scan_queue_size_max_allowed`, + help: `Maximum number of task in the scan queue`, + labelNames: ['resource', 'pollingGroup'], + registers: [registry], + }); + + return { + scan_cycles_total, + scan_overruns_total, + scan_overruns_consecutive, + scan_task_total, + scan_task_off_scan_total, + scan_cycle_duration_milliseconds, + scan_cycles_on_stats, + scan_duration_avg_milliseconds, + scan_duration_max_milliseconds, + scan_duration_min_milliseconds, + scan_queue_size_max_allowed, + }; +}; + +/** Metrics definitions */ +export type MetricsDefinitions = TaskMetricsDefinitions & ScanMetricsDefinitions; + +/** Metrics definitions */ +export const METRICS_DEFINITIONS = (registry: Registry): MetricsDefinitions => { + return { + ...TASK_METRICS_DEFINITIONS(registry), + ...SCAN_METRICS_DEFINITIONS(registry), + }; +}; diff --git a/packages/api/tasks/src/Polling/types/PollingManagerOptions.i.ts b/packages/api/tasks/src/Polling/types/PollingManagerOptions.i.ts index 2cf7bcca..da1f77c5 100644 --- a/packages/api/tasks/src/Polling/types/PollingManagerOptions.i.ts +++ b/packages/api/tasks/src/Polling/types/PollingManagerOptions.i.ts @@ -22,12 +22,12 @@ export interface PollingManagerOptions { entries: TaskBaseConfig[]; /** * Number of fast cycles to run per slow cycle - * @default 3 + * @defaultValue 3 */ slowCycleRatio?: number; /** * Number of cycles on stats - * @default 10 + * @defaultValue 10 */ cyclesOnStats?: number; } diff --git a/packages/api/tasks/src/Scheduler/Scheduler.test.ts b/packages/api/tasks/src/Scheduler/Scheduler.test.ts index 4b7cf834..7f517347 100644 --- a/packages/api/tasks/src/Scheduler/Scheduler.test.ts +++ b/packages/api/tasks/src/Scheduler/Scheduler.test.ts @@ -920,7 +920,7 @@ describe('#Scheduler', () => { } catch (error) { expect(error).toBeInstanceOf(Crash); expect((error as Crash).message).toBe( - 'Pattern should be an object an object with the task property: {\n "pattern": []\n}' + 'Pattern should be an object with the task property: {\n "pattern": []\n}' ); } try { diff --git a/packages/api/tasks/src/Scheduler/Scheduler.ts b/packages/api/tasks/src/Scheduler/Scheduler.ts index a7c1b833..b75d511f 100644 --- a/packages/api/tasks/src/Scheduler/Scheduler.ts +++ b/packages/api/tasks/src/Scheduler/Scheduler.ts @@ -143,7 +143,7 @@ export class Scheduler< * Event handler for error events * @param error - Error event */ - private onError = (error: Crash | Multi): void => { + private readonly onError = (error: Crash | Multi): void => { if (this.listenerCount('error') > 0) { this.emit('error', error); } @@ -155,7 +155,12 @@ export class Scheduler< * @param meta - Task metadata * @param error - Task error */ - private onDone = (uuid: string, result: any, meta: MetaData, error?: Crash | Multi): void => { + private readonly onDone = ( + uuid: string, + result: any, + meta: MetaData, + error?: Crash | Multi + ): void => { this.emit('done', uuid, result, meta, error); }; /** @@ -283,7 +288,7 @@ export class Scheduler< } for (const resource of this.pollingExecutors.values()) { for (const manager of resource.values()) { - await manager.stop(); + manager.stop(); manager.off('error', this.onError); manager.off('done', this.onDone); } diff --git a/packages/api/tasks/src/Scheduler/types/SchedulerOptions.i.ts b/packages/api/tasks/src/Scheduler/types/SchedulerOptions.i.ts index c2b5ab32..70d93f9c 100644 --- a/packages/api/tasks/src/Scheduler/types/SchedulerOptions.i.ts +++ b/packages/api/tasks/src/Scheduler/types/SchedulerOptions.i.ts @@ -48,12 +48,12 @@ export interface SchedulerOptions< limiterOptions?: LimiterOptions; /** * Number of fast cycles to run per slow cycle - * @default 3 + * @defaultValue 3 */ slowCycleRatio?: number; /** * Number of cycles on stats - * @default 10 + * @defaultValue 10 */ cyclesOnStats?: number; } diff --git a/packages/api/tasks/src/Tasks/Group.ts b/packages/api/tasks/src/Tasks/Group.ts index 1838496a..799ab2bb 100644 --- a/packages/api/tasks/src/Tasks/Group.ts +++ b/packages/api/tasks/src/Tasks/Group.ts @@ -1,73 +1,75 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { TaskHandler } from './TaskHandler'; -import { TaskOptions } from './types'; - -export class Group extends TaskHandler<(T | null)[], U> { - /** Results of the sequence */ - private readonly results: (T | null)[] = []; - /** Error of the group of tasks */ - private error?: Multi; - /** - * Create a new task handler for a group of tasks - * @param tasks - The tasks to execute - * @param options - The options for the task - * @param atLeastOne - If at least one task must succeed to consider the group as successful - * execution, in other case, all the tasks must succeed - */ - constructor( - private readonly tasks: TaskHandler[], - options?: TaskOptions, - private readonly atLeastOne?: boolean - ) { - super(options); - } - /** Execute the task */ - protected async _execute(): Promise<(T | null)[]> { - for (const task of this.tasks) { - await this.unitaryExecution(task); - } - if ((!this.atLeastOne && this.error) || (this.atLeastOne && this.allTasksWithErrors)) { - throw this.error; - } else { - return this.results; - } - } - /** Check if all the tasks have failed */ - private get allTasksWithErrors(): boolean { - return ( - typeof this.error !== 'undefined' && - Array.isArray(this.error.causes) && - this.tasks.length === this.error.causes.length - ); - } - /** - * Execute a task and handle the result - * @param task - The task to execute - */ - private async unitaryExecution(task: TaskHandler): Promise { - try { - const result = await task.execute(); - this.results.push(result); - this._$meta.push(task.metadata); - } catch (rawError) { - const cause = Crash.from(rawError); - if (!this.error) { - this.error = new Multi(`At least one of the task grouped failed`, { - causes: [cause], - }); - this._reason = this.error.message; - } else { - this.error.push(cause); - } - this.results.push(null); - this._$meta.push(task.metadata); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { TaskHandler } from './TaskHandler'; +import { TaskOptions } from './types'; + +export class Group extends TaskHandler<(T | null)[], U> { + /** Results of the sequence */ + private readonly results: (T | null)[] = []; + /** Error of the group of tasks */ + private error?: Multi; + /** + * Create a new task handler for a group of tasks + * @param tasks - The tasks to execute + * @param options - The options for the task + * @param atLeastOne - If at least one task must succeed to consider the group as successful + * execution, in other case, all the tasks must succeed + */ + constructor( + private readonly tasks: TaskHandler[], + options?: TaskOptions, + private readonly atLeastOne?: boolean + ) { + super(options); + } + /** Execute the task */ + protected async _execute(): Promise<(T | null)[]> { + this.error = undefined; + for (const task of this.tasks) { + await this.unitaryExecution(task); + } + if ((!this.atLeastOne && this.error) || (this.atLeastOne && this.allTasksWithErrors)) { + throw this.error; + } else { + return this.results; + } + } + /** Check if all the tasks have failed */ + private get allTasksWithErrors(): boolean { + return ( + typeof this.error !== 'undefined' && + Array.isArray(this.error.causes) && + this.tasks.length === this.error.causes.length + ); + } + /** + * Execute a task and handle the result + * @param task - The task to execute + */ + private async unitaryExecution(task: TaskHandler): Promise { + try { + const result = await task.execute(); + this.results.push(result); + this._$meta.push(task.metadata); + } catch (rawError) { + const cause = Crash.from(rawError); + if (!this.error) { + this.error = new Multi(`At least one of the task grouped failed`, { + causes: [cause], + }); + this._reason = this.error.message; + } else { + this.error.push(cause); + } + this.results.push(null); + this._$meta.push(task.metadata); + } + } +} + diff --git a/packages/api/tasks/src/Tasks/Sequence.ts b/packages/api/tasks/src/Tasks/Sequence.ts index fd0c9daf..5ea4ac9e 100644 --- a/packages/api/tasks/src/Tasks/Sequence.ts +++ b/packages/api/tasks/src/Tasks/Sequence.ts @@ -1,51 +1,52 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import { TaskHandler } from './TaskHandler'; -import { SequencePattern, TaskOptions } from './types'; - -export class Sequence extends TaskHandler { - /** - * Create a new task handler for a sequence of tasks - * @param pattern - The pattern for the sequence - * @param options - The options for the task - */ - constructor( - private readonly pattern: SequencePattern, - options?: TaskOptions - ) { - super(options); - } - /** Execute the task */ - protected async _execute(): Promise { - try { - await this.executePhase(this.pattern.pre, 'pre'); - const result = await this.pattern.task.execute(); - this._$meta.push(this.pattern.task.metadata); - await this.executePhase(this.pattern.post, 'post'); - return result; - } catch (error) { - throw error; - } finally { - await this.executePhase(this.pattern.finally, 'finally'); - } - } - /** Execute a phase of the sequence */ - private async executePhase(tasks: TaskHandler[] = [], phase: string): Promise { - for (const task of tasks) { - try { - await task.execute(); - } catch (rawError) { - const cause = Crash.from(rawError); - throw new Crash(`Error executing the [${phase}] phase: ${cause.message}`, { cause }); - } finally { - this._$meta.push(task.metadata); - } - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { TaskHandler } from './TaskHandler'; +import { SequencePattern, TaskOptions } from './types'; + +export class Sequence extends TaskHandler { + /** + * Create a new task handler for a sequence of tasks + * @param pattern - The pattern for the sequence + * @param options - The options for the task + */ + constructor( + private readonly pattern: SequencePattern, + options?: TaskOptions + ) { + super(options); + } + /** Execute the task */ + protected async _execute(): Promise { + try { + await this.executePhase(this.pattern.pre, 'pre'); + const result = await this.pattern.task.execute(); + this._$meta.push(this.pattern.task.metadata); + await this.executePhase(this.pattern.post, 'post'); + return result; + } catch (error) { + throw Crash.from(error); + } finally { + await this.executePhase(this.pattern.finally, 'finally'); + } + } + /** Execute a phase of the sequence */ + private async executePhase(tasks: TaskHandler[] = [], phase: string): Promise { + for (const task of tasks) { + try { + await task.execute(); + } catch (rawError) { + const cause = Crash.from(rawError); + throw new Crash(`Error executing the [${phase}] phase: ${cause.message}`, { cause }); + } finally { + this._$meta.push(task.metadata); + } + } + } +} + diff --git a/packages/api/tasks/src/Tasks/TaskHandler.ts b/packages/api/tasks/src/Tasks/TaskHandler.ts index 6d5b70ce..f9a9851d 100644 --- a/packages/api/tasks/src/Tasks/TaskHandler.ts +++ b/packages/api/tasks/src/Tasks/TaskHandler.ts @@ -1,299 +1,298 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { RetryOptions } from '@mdf.js/utils'; -import { EventEmitter } from 'events'; -import { cloneDeep } from 'lodash'; -import { v4 } from 'uuid'; -import { - DEFAULT_PRIORITY, - DEFAULT_RETRY_OPTIONS, - DEFAULT_RETRY_STRATEGY, - DEFAULT_WEIGHT, - DoneEventHandler, - MetaData, - RETRY_STRATEGY, - RetryStrategy, - TASK_STATE, - TaskOptions, - TaskState, -} from './types'; - -/** Represents the task handler */ -export declare interface TaskHandler { - /** - * Register an event listener over the `done` event, which is emitted when a task has ended, either - * due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - on(event: 'done', listener: DoneEventHandler): this; - /** - * Register an event listener over the `done` event, which is emitted when a task has ended, either - * due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - addListener(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a event listener over the `done` event, at the beginning of the listeners array, - * which is emitted when a task has ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - prependListener(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, which is emitted when a task has - * ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - once(event: 'done', listener: DoneEventHandler): this; - /** - * Registers a one-time event listener over the `done` event, at the beginning of the listeners - * array, which is emitted when a task has ended, either due to completion or failure. - * @param uuid - The unique identifier of the task - * @param result - The result of the task - * @param meta - The {@link MetaData} information of the task, including all the relevant information - * @param error - The error of the task, if any - */ - prependOnceListener(event: 'done', listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - removeListener(event: 'done', listener: DoneEventHandler): this; - /** - * Removes the specified listener from the listener array for the `done` event. - * @param event - `done` event - * @param listener - The listener function to remove - */ - off(event: 'done', listener: DoneEventHandler): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `done` event - */ - removeAllListeners(event?: 'done'): this; -} - -/** Represents the task handler */ -export abstract class TaskHandler extends EventEmitter { - /** Unique task identification, unique for each task */ - public readonly uuid: string; - /** Task identifier, defined by the user */ - public readonly taskId: string; - /** Date when the task was created */ - public readonly createdAt: Date; - /** Date when the task was executed in ISO format */ - protected executedAt?: Date; - /** Date when the task was completed in ISO format */ - protected completedAt?: Date; - /** Date when the task was cancelled in ISO format */ - protected cancelledAt?: Date; - /** Date when the task was failed in ISO format */ - protected failedAt?: Date; - /** Reason of failure or cancellation */ - protected _reason?: string; - /** Additional metadata in case the execution required execute other tasks */ - protected readonly _$meta: MetaData[] = []; - /** Task priority */ - public readonly priority: number; - /** Task weight */ - public readonly weight: number; - /** Status of the task */ - private status: TaskState = TASK_STATE.PENDING; - /** Retry options */ - protected retryOptions: RetryOptions; - /** Context to be bind to the task */ - protected context?: Binded; - /** Result of the task */ - private result?: Result; - /** Strategy to retry the task */ - private readonly strategy: RetryStrategy; - /** - * Create a new task handler - * @param options - The options for the task - */ - constructor(options: TaskOptions = {}) { - super(); - this.uuid = v4(); - this.createdAt = new Date(); - this.priority = options.priority ?? DEFAULT_PRIORITY; - this.weight = options.weight ?? DEFAULT_WEIGHT; - this.taskId = options.id || v4(); - this.retryOptions = options.retryOptions || DEFAULT_RETRY_OPTIONS; - this.context = options.bind; - this.strategy = options.retryStrategy || DEFAULT_RETRY_STRATEGY; - } - /** - * Get the cause of the error - * @param error - The error to get the cause - * @returns The cause of the error - */ - private getCause(error: Crash | Multi): string { - //Erro from group of tasks - if (error instanceof Multi) { - return error.trace().join(',\n') || error.message; - } //Error from retry or retryBind - else if (error.name === 'InterruptionError' || error.name === 'AbortError') { - return error.cause ? error.cause.message : error.message; - } //Error from task or sequence phase - else { - return error.message; - } - } - /** - * Execute the task - * @returns The result of the task - */ - private shouldBeExecuted(): boolean { - // Execute the task if it is pending - if (this.status === TASK_STATE.PENDING) { - this.status = TASK_STATE.RUNNING; - this.executedAt = new Date(); - return true; - } - switch (this.strategy) { - case RETRY_STRATEGY.FAIL_AFTER_EXECUTED: - this.onRetry(); - this.onError( - new Crash( - `Task [${this.taskId}] was executed previously, you can't execute it again due to the retry strategy.`, - this.uuid, - { info: this.metadata } - ) - ); - case RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS: - if (this.status === TASK_STATE.COMPLETED) { - return false; - } - this.onRetry(); - return true; - case RETRY_STRATEGY.FAIL_AFTER_SUCCESS: - if (this.status === TASK_STATE.COMPLETED) { - this.onRetry(); - this.onError( - new Crash( - `Task [${this.taskId}] was previously executed successfully, you can't execute it again due to the retry strategy.`, - this.uuid, - { info: this.metadata } - ) - ); - } - this.onRetry(); - return true; - case RETRY_STRATEGY.RETRY: - default: - this.onRetry(); - return true; - } - } - /** - * Handle the error - * @param rawError - The error to handle - * @returns The error - */ - private onError(rawError: Crash | Multi): never { - const cause = Crash.from(rawError); - this.status = cause.name === 'AbortError' ? TASK_STATE.CANCELLED : TASK_STATE.FAILED; - if (this.status === TASK_STATE.CANCELLED) { - this.cancelledAt = new Date(); - } else { - this.failedAt = new Date(); - } - this._reason = `Execution error in task [${this.taskId}]: ${this.getCause(cause)}`; - const error = new Crash(this._reason, this.uuid, { cause, info: this.metadata }); - this.done(undefined, error); - throw error; - } - /** - * Handle the success - * @param result - The result of the task - * @returns The result - */ - private onSuccess(result: Result): Result { - this.status = TASK_STATE.COMPLETED; - this.completedAt = new Date(); - this.result = result; - this.done(this.result); - return result; - } - /** Handle the retry */ - private onRetry(): void { - this._$meta.push(cloneDeep(this.metadata)); - this.status = TASK_STATE.PENDING; - this.executedAt = new Date(); - this._reason = undefined; - this.completedAt = undefined; - this.cancelledAt = undefined; - this.failedAt = undefined; - } - /** Return the duration of the task */ - private get duration(): number { - const executedAt = this.executedAt?.getTime(); - const completedAt = - this.completedAt?.getTime() || this.cancelledAt?.getTime() || this.failedAt?.getTime(); - if (!executedAt || !completedAt) { - return -1; - } - return completedAt - executedAt; - } - /** Notify that the task has been processed */ - private done(result?: Result, error?: Multi | Crash): void { - if (this.listenerCount('done') <= 0) { - return; - } - this.emit('done', this.uuid, result, this.metadata, error); - } - /** Return the metadata of the task */ - public get metadata(): MetaData { - return { - uuid: this.uuid, - taskId: this.taskId, - status: this.status, - createdAt: this.createdAt.toISOString(), - executedAt: this.executedAt?.toISOString(), - completedAt: this.completedAt?.toISOString(), - cancelledAt: this.cancelledAt?.toISOString(), - failedAt: this.failedAt?.toISOString(), - reason: this._reason, - duration: this.duration, - priority: this.priority, - weight: this.weight, - $meta: this._$meta, - }; - } - /** Execute the task */ - public async execute(): Promise { - if (this.shouldBeExecuted()) { - return this._execute().then(this.onSuccess.bind(this)).catch(this.onError.bind(this)); - } - return this.result as Result; - } - /** Cancel the task */ - public cancel(error?: Crash): void { - const cause = error || new Crash(`Task [${this.taskId}] was cancelled by the user`, this.uuid); - cause.name = 'AbortError'; - try { - this.onError(cause); - } catch { - // Do nothing - } - } - /** Execute the underlayer execution strategy */ - protected abstract _execute(): Promise; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { RetryOptions } from '@mdf.js/utils'; +import { EventEmitter } from 'events'; +import { cloneDeep } from 'lodash'; +import { v4 } from 'uuid'; +import { + DEFAULT_PRIORITY, + DEFAULT_RETRY_OPTIONS, + DEFAULT_RETRY_STRATEGY, + DEFAULT_WEIGHT, + DoneEventHandler, + MetaData, + RETRY_STRATEGY, + RetryStrategy, + TASK_STATE, + TaskOptions, + TaskState, +} from './types'; + +/** Represents the task handler */ +export declare interface TaskHandler { + /** + * Register an event listener over the `done` event, which is emitted when a task has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + on(event: 'done', listener: DoneEventHandler): this; + /** + * Register an event listener over the `done` event, which is emitted when a task has ended, either + * due to completion or failure. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + addListener(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a event listener over the `done` event, at the beginning of the listeners array, + * which is emitted when a task has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + prependListener(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, which is emitted when a task has + * ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + once(event: 'done', listener: DoneEventHandler): this; + /** + * Registers a one-time event listener over the `done` event, at the beginning of the listeners + * array, which is emitted when a task has ended, either due to completion or failure. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + prependOnceListener(event: 'done', listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + removeListener(event: 'done', listener: DoneEventHandler): this; + /** + * Removes the specified listener from the listener array for the `done` event. + * @param event - `done` event + * @param listener - Done event handler + * @event + */ + off(event: 'done', listener: DoneEventHandler): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `done` event + * @event + */ + removeAllListeners(event?: 'done'): this; +} + +/** Represents the task handler */ +export abstract class TaskHandler extends EventEmitter { + /** Unique task identification, unique for each task */ + public readonly uuid: string; + /** Task identifier, defined by the user */ + public readonly taskId: string; + /** Date when the task was created */ + public readonly createdAt: Date; + /** Date when the task was executed in ISO format */ + protected executedAt?: Date; + /** Date when the task was completed in ISO format */ + protected completedAt?: Date; + /** Date when the task was cancelled in ISO format */ + protected cancelledAt?: Date; + /** Date when the task was failed in ISO format */ + protected failedAt?: Date; + /** Reason of failure or cancellation */ + protected _reason?: string; + /** Additional metadata in case the execution required execute other tasks */ + protected readonly _$meta: MetaData[] = []; + /** Task priority */ + public readonly priority: number; + /** Task weight */ + public readonly weight: number; + /** Status of the task */ + private status: TaskState = TASK_STATE.PENDING; + /** Retry options */ + protected retryOptions: RetryOptions; + /** Context to be bind to the task */ + protected context?: Binded; + /** Result of the task */ + private result?: Result; + /** Strategy to retry the task */ + private readonly strategy: RetryStrategy; + /** + * Create a new task handler + * @param options - The options for the task + */ + constructor(options: TaskOptions = {}) { + super(); + this.uuid = v4(); + this.createdAt = new Date(); + this.priority = options.priority ?? DEFAULT_PRIORITY; + this.weight = options.weight ?? DEFAULT_WEIGHT; + this.taskId = options.id ?? v4(); + this.retryOptions = options.retryOptions ?? DEFAULT_RETRY_OPTIONS; + this.context = options.bind; + this.strategy = options.retryStrategy ?? DEFAULT_RETRY_STRATEGY; + } + /** + * Get the cause of the error + * @param error - The error to get the cause + * @returns The cause of the error + */ + private getCause(error: Crash | Multi): string { + //Erro from group of tasks + if (error instanceof Multi) { + return error.trace().join(',\n') || error.message; + } //Error from retry or retryBind + else if (error.name === 'InterruptionError' || error.name === 'AbortError') { + return error.cause ? error.cause.message : error.message; + } //Error from task or sequence phase + else { + return error.message; + } + } + /** + * Execute the task + * @returns The result of the task + */ + private shouldBeExecuted(): boolean { + // Execute the task if it is pending + if (this.status === TASK_STATE.PENDING) { + this.status = TASK_STATE.RUNNING; + this.executedAt = new Date(); + return true; + } + switch (this.strategy) { + case RETRY_STRATEGY.FAIL_AFTER_EXECUTED: + this.onRetry(); + this.onError( + new Crash( + `Task [${this.taskId}] was executed previously, you can't execute it again due to the retry strategy.`, + this.uuid, + { info: this.metadata } + ) + ); + break; + case RETRY_STRATEGY.NOT_EXEC_AFTER_SUCCESS: + if (this.status === TASK_STATE.COMPLETED) { + return false; + } + this.onRetry(); + return true; + case RETRY_STRATEGY.FAIL_AFTER_SUCCESS: + if (this.status === TASK_STATE.COMPLETED) { + this.onRetry(); + this.onError( + new Crash( + `Task [${this.taskId}] was previously executed successfully, you can't execute it again due to the retry strategy.`, + this.uuid, + { info: this.metadata } + ) + ); + } + this.onRetry(); + return true; + case RETRY_STRATEGY.RETRY: + default: + this.onRetry(); + return true; + } + } + /** + * Handle the error + * @param rawError - The error to handle + * @returns The error + */ + private onError(rawError: Crash | Multi): never { + const cause = Crash.from(rawError); + this.status = cause.name === 'AbortError' ? TASK_STATE.CANCELLED : TASK_STATE.FAILED; + if (this.status === TASK_STATE.CANCELLED) { + this.cancelledAt = new Date(); + } else { + this.failedAt = new Date(); + } + this._reason = `Execution error in task [${this.taskId}]: ${this.getCause(cause)}`; + const error = new Crash(this._reason, this.uuid, { cause, info: this.metadata }); + this.done(undefined, error); + throw error; + } + /** + * Handle the success + * @param result - The result of the task + * @returns The result + */ + private onSuccess(result: Result): Result { + this.status = TASK_STATE.COMPLETED; + this.completedAt = new Date(); + this.result = result; + this.done(this.result); + return result; + } + /** Handle the retry */ + private onRetry(): void { + this._$meta.push(cloneDeep(this.metadata)); + this.status = TASK_STATE.PENDING; + this.executedAt = new Date(); + this._reason = undefined; + this.completedAt = undefined; + this.cancelledAt = undefined; + this.failedAt = undefined; + } + /** Return the duration of the task */ + private get duration(): number { + const executedAt = this.executedAt?.getTime(); + const completedAt = + this.completedAt?.getTime() ?? this.cancelledAt?.getTime() ?? this.failedAt?.getTime(); + if (!executedAt || !completedAt) { + return -1; + } + return completedAt - executedAt; + } + /** Notify that the task has been processed */ + private done(result?: Result, error?: Multi | Crash): void { + if (this.listenerCount('done') <= 0) { + return; + } + this.emit('done', this.uuid, result, this.metadata, error); + } + /** Return the metadata of the task */ + public get metadata(): MetaData { + return { + uuid: this.uuid, + taskId: this.taskId, + status: this.status, + createdAt: this.createdAt.toISOString(), + executedAt: this.executedAt?.toISOString(), + completedAt: this.completedAt?.toISOString(), + cancelledAt: this.cancelledAt?.toISOString(), + failedAt: this.failedAt?.toISOString(), + reason: this._reason, + duration: this.duration, + priority: this.priority, + weight: this.weight, + $meta: this._$meta, + }; + } + /** Execute the task */ + public async execute(): Promise { + if (this.shouldBeExecuted()) { + return this._execute().then(this.onSuccess.bind(this)).catch(this.onError.bind(this)); + } + return this.result as Result; + } + /** Cancel the task */ + public cancel(error?: Crash): void { + const cause = error || new Crash(`Task [${this.taskId}] was cancelled by the user`, this.uuid); + cause.name = 'AbortError'; + try { + this.onError(cause); + } catch { + // Do nothing + } + } + /** Execute the underlayer execution strategy */ + protected abstract _execute(): Promise; +} diff --git a/packages/api/tasks/src/Tasks/types/TaskOptions.i.ts b/packages/api/tasks/src/Tasks/types/TaskOptions.i.ts index a7450afd..2fe69d97 100644 --- a/packages/api/tasks/src/Tasks/types/TaskOptions.i.ts +++ b/packages/api/tasks/src/Tasks/types/TaskOptions.i.ts @@ -1,43 +1,44 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { RetryOptions } from '@mdf.js/utils'; -import { RetryStrategy } from './RetryStrategy.t'; - -/** Represents the options for a task */ -export interface TaskOptions { - /** - * The priority of the task. A higher value means a higher priority. The default priority is 0. - * @default 0 - */ - priority?: number; - /** - * The weight of the task, this define the number of tokens that the task will consume from the - * bucket. The default weight is 1. - * @default 1 - */ - weight?: number; - /** - * Task identifier, it necessary to identify the task during all the process, for example, when - * the job is executed, the event with the task identifier will be emitted with the result of the - * task. - * @default If not provided, the task identifier will be generated automatically. - */ - id?: string; - /** - * Set the options for the retry process of the task - * @default undefined - */ - retryOptions?: RetryOptions; - /** - * Set the strategy to retry the task - * @default RETRY_STRATEGY.RETRY - */ - retryStrategy?: RetryStrategy; - /** Context to be bind to the task */ - bind?: U; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { RetryOptions } from '@mdf.js/utils'; +import { RetryStrategy } from './RetryStrategy.t'; + +/** Represents the options for a task */ +export interface TaskOptions { + /** + * The priority of the task. A higher value means a higher priority. The default priority is 0. + * @defaultValue 0 + */ + priority?: number; + /** + * The weight of the task, this define the number of tokens that the task will consume from the + * bucket. The default weight is 1. + * @defaultValue 1 + */ + weight?: number; + /** + * Task identifier, it necessary to identify the task during all the process, for example, when + * the job is executed, the event with the task identifier will be emitted with the result of the + * task. + * @defaultValue If not provided, the task identifier will be generated automatically. + */ + id?: string; + /** + * Set the options for the retry process of the task + * @defaultValue undefined + */ + retryOptions?: RetryOptions; + /** + * Set the strategy to retry the task + * @defaultValue RETRY_STRATEGY.RETRY + */ + retryStrategy?: RetryStrategy; + /** Context to be bind to the task */ + bind?: U; +} + diff --git a/packages/api/tasks/src/index.ts b/packages/api/tasks/src/index.ts index 7d5fdf19..3193927a 100644 --- a/packages/api/tasks/src/index.ts +++ b/packages/api/tasks/src/index.ts @@ -1,50 +1,53 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { - Limiter, - LimiterOptions, - LimiterState, - QueueOptions, - STRATEGIES, - STRATEGY, - Strategy, -} from './Limiter'; -export { - DefaultPollingGroups, - GroupTaskBaseConfig, - MetricsDefinitions, - PollingExecutor, - PollingGroup, - PollingManagerOptions, - PollingStats, - SequenceTaskBaseConfig, - SingleTaskBaseConfig, - TaskBaseConfig, - WellIdentifiedTaskOptions, -} from './Polling'; -export { - ResourceConfigEntry, - ResourcesConfigObject, - Scheduler, - SchedulerOptions, -} from './Scheduler'; -export { - DoneEventHandler as DoneListener, - Group, - MetaData, - RETRY_STRATEGY, - RetryStrategy, - Sequence, - SequencePattern, - Single, - TASK_STATE, - TASK_STATES, - TaskHandler, - TaskOptions, - TaskState, -} from './Tasks'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { + ConsolidatedLimiterOptions, + Limiter, + LimiterOptions, + LimiterState, + QueueOptions, + STRATEGIES, + STRATEGY, + Strategy, +} from './Limiter'; +export { + DefaultPollingGroups, + GroupTaskBaseConfig, + MetricsDefinitions, + PollingExecutor, + PollingGroup, + PollingManagerOptions, + PollingStats, + ScanMetricsDefinitions, + SequenceTaskBaseConfig, + SingleTaskBaseConfig, + TaskBaseConfig, + TaskMetricsDefinitions, + WellIdentifiedTaskOptions, +} from './Polling'; +export { + ResourceConfigEntry, + ResourcesConfigObject, + Scheduler, + SchedulerOptions, +} from './Scheduler'; +export { + DoneEventHandler as DoneListener, + Group, + MetaData, + RETRY_STRATEGY, + RetryStrategy, + Sequence, + SequencePattern, + Single, + TASK_STATE, + TASK_STATES, + TaskHandler, + TaskOptions, + TaskState, +} from './Tasks'; diff --git a/packages/api/utils/README.md b/packages/api/utils/README.md index 1058d13e..f78f86f4 100644 --- a/packages/api/utils/README.md +++ b/packages/api/utils/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/api/utils/package.json b/packages/api/utils/package.json index 9fec4d00..be7c6a72 100644 --- a/packages/api/utils/package.json +++ b/packages/api/utils/package.json @@ -1,52 +1,51 @@ -{ - "name": "@mdf.js/utils", - "version": "0.0.1", - "description": "MMS - API Core - Common utils tools", - "keywords": [ - "NodeJS", - "MMS", - "API", - "coerce", - "escape", - "retry", - "validateError" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/api/utils" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/utils/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/crash": "*", - "lodash": "^4.17.21", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/utils", + "version": "0.0.1", + "description": "MMS - API Core - Common utils tools", + "keywords": [ + "NodeJS", + "MMS", + "API", + "coerce", + "escape", + "retry", + "validateError" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/api/utils" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/api/utils/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/crash": "*", + "lodash": "^4.17.21", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/api/utils/src/camelCase/Options.i.ts b/packages/api/utils/src/camelCase/Options.i.ts index 851a6c4b..ebb4040f 100644 --- a/packages/api/utils/src/camelCase/Options.i.ts +++ b/packages/api/utils/src/camelCase/Options.i.ts @@ -1,42 +1,43 @@ -/* eslint-disable max-len */ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export interface Options { - /** - * Uppercase the first character: `foo-bar` → `FooBar`. - * @default false - */ - readonly pascalCase?: boolean; - /** - * Preserve consecutive uppercase characters: `foo-BAR` → `fooBAR`. - * @default false - */ - readonly preserveConsecutiveUppercase?: boolean; - /** - * The locale parameter indicates the locale to be used to convert to upper/lower case according - * to any locale-specific case mappings. If multiple locales are given in an array, the best - * available locale is used. - * Setting `locale: false` ignores the platform locale and uses the - * [Unicode Default Case Conversion](https://unicode-org.github.io/icu/userguide/transforms/casemappings.html#simple-single-character-case-mapping) - * algorithm. - * Default: The host environment’s current locale. - * @example - * ```ts - * import { camelCase } from 'camelCase'; - * camelCase('lorem-ipsum', {locale: 'en-US'}); - * //=> 'loremIpsum' - * camelCase('lorem-ipsum', {locale: 'tr-TR'}); - * //=> 'loremİpsum' - * camelCase('lorem-ipsum', {locale: ['en-US', 'en-GB']}); - * //=> 'loremIpsum' - * camelCase('lorem-ipsum', {locale: ['tr', 'TR', 'tr-TR']}); - * //=> 'loremİpsum' - * ``` - */ - readonly locale?: string | string[]; -} +/* eslint-disable max-len */ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export interface Options { + /** + * Uppercase the first character: `foo-bar` → `FooBar`. + * @defaultValue false + */ + readonly pascalCase?: boolean; + /** + * Preserve consecutive uppercase characters: `foo-BAR` → `fooBAR`. + * @defaultValue false + */ + readonly preserveConsecutiveUppercase?: boolean; + /** + * The locale parameter indicates the locale to be used to convert to upper/lower case according + * to any locale-specific case mappings. If multiple locales are given in an array, the best + * available locale is used. + * Setting `locale: false` ignores the platform locale and uses the + * [Unicode Default Case Conversion](https://unicode-org.github.io/icu/userguide/transforms/casemappings.html#simple-single-character-case-mapping) + * algorithm. + * Default: The host environment’s current locale. + * @example + * ```ts + * import { camelCase } from 'camelCase'; + * camelCase('lorem-ipsum', {locale: 'en-US'}); + * //=> 'loremIpsum' + * camelCase('lorem-ipsum', {locale: 'tr-TR'}); + * //=> 'loremİpsum' + * camelCase('lorem-ipsum', {locale: ['en-US', 'en-GB']}); + * //=> 'loremIpsum' + * camelCase('lorem-ipsum', {locale: ['tr', 'TR', 'tr-TR']}); + * //=> 'loremİpsum' + * ``` + */ + readonly locale?: string | string[]; +} + diff --git a/packages/api/utils/src/coerce/coerce.ts b/packages/api/utils/src/coerce/coerce.ts index ba158311..eb4ff42d 100644 --- a/packages/api/utils/src/coerce/coerce.ts +++ b/packages/api/utils/src/coerce/coerce.ts @@ -1,68 +1,69 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -type Coerceable = boolean | number | Record | any[] | null; -/** - * Coerce an environment variable - * @param env - Environment - */ -export function coerce(env: string | undefined): Coerceable; -/** - * Coerce an environment variable to a boolean value - * @param env - Environment - * @param alternative - default value - */ -export function coerce(env: string | undefined, alternative: boolean): boolean; -/** - * Coerce an environment variable to a numerical value - * @param env - Environment - * @param alternative - default value - */ -export function coerce(env: string | undefined, alternative: number): number; -/** - * Coerce an environment variable to an object - * @param env - Environment - * @param alternative - default value - */ -export function coerce( - env: string | undefined, - alternative: Record -): Record; -/** - * Coerce an environment variable to an array - * @param env - Environment - * @param alternative - default value - */ -export function coerce(env: string | undefined, alternative: any[]): any[]; -/** - * Coerce an environment variable to a valid value - * @param env - Environment - */ -export function coerce(env: string | undefined): T | undefined; -/** - * Coerce an environment variable to a valid value - * @param env - Environment - * @param alternative - default value - */ -export function coerce(env: string | undefined, alternative: any): any; -export function coerce(env: string | undefined, alternative?: Coerceable): Coerceable | undefined { - if (typeof env !== 'string') { - return alternative; - } else if (env.toLowerCase() === 'true') { - return true; - } else if (env.toLowerCase() === 'false') { - return false; - } else if (env.toLowerCase() === 'null') { - return null; - } else { - try { - return JSON.parse(env); - } catch (error) { - return alternative; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Coercible types */ +export type Coercible = boolean | number | Record | any[] | null; +/** + * Coerce an environment variable + * @param env - Environment + */ +export function coerce(env: string | undefined): Coercible; +/** + * Coerce an environment variable to a boolean value + * @param env - Environment + * @param alternative - default value + */ +export function coerce(env: string | undefined, alternative: boolean): boolean; +/** + * Coerce an environment variable to a numerical value + * @param env - Environment + * @param alternative - default value + */ +export function coerce(env: string | undefined, alternative: number): number; +/** + * Coerce an environment variable to an object + * @param env - Environment + * @param alternative - default value + */ +export function coerce( + env: string | undefined, + alternative: Record +): Record; +/** + * Coerce an environment variable to an array + * @param env - Environment + * @param alternative - default value + */ +export function coerce(env: string | undefined, alternative: any[]): any[]; +/** + * Coerce an environment variable to a valid value + * @param env - Environment + */ +export function coerce(env: string | undefined): T | undefined; +/** + * Coerce an environment variable to a valid value + * @param env - Environment + * @param alternative - default value + */ +export function coerce(env: string | undefined, alternative: any): any; +export function coerce(env: string | undefined, alternative?: Coercible): Coercible | undefined { + if (typeof env !== 'string') { + return alternative; + } else if (env.toLowerCase() === 'true') { + return true; + } else if (env.toLowerCase() === 'false') { + return false; + } else if (env.toLowerCase() === 'null') { + return null; + } else { + try { + return JSON.parse(env); + } catch (error) { + return alternative; + } + } +} diff --git a/packages/api/utils/src/formatEnv/index.ts b/packages/api/utils/src/formatEnv/index.ts index efdd9de6..5f9b75ed 100644 --- a/packages/api/utils/src/formatEnv/index.ts +++ b/packages/api/utils/src/formatEnv/index.ts @@ -1,8 +1,10 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export * from './formatEnv'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './formatEnv'; +export * from './types'; + diff --git a/packages/api/utils/src/formatEnv/types/Format.t.ts b/packages/api/utils/src/formatEnv/types/Format.t.ts index 89c0be6b..20ac8be8 100644 --- a/packages/api/utils/src/formatEnv/types/Format.t.ts +++ b/packages/api/utils/src/formatEnv/types/Format.t.ts @@ -1,8 +1,10 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export type Format = 'camelcase' | 'pascalcase' | 'lowercase' | 'uppercase'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** The format to apply to the environment variable name. */ +export type Format = 'camelcase' | 'pascalcase' | 'lowercase' | 'uppercase'; + diff --git a/packages/api/utils/src/formatEnv/types/FormatFunction.t.ts b/packages/api/utils/src/formatEnv/types/FormatFunction.t.ts index ea8d4b5d..d81107fc 100644 --- a/packages/api/utils/src/formatEnv/types/FormatFunction.t.ts +++ b/packages/api/utils/src/formatEnv/types/FormatFunction.t.ts @@ -1,8 +1,10 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export type FormatFunction = (str: string) => string; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Format function type */ +export type FormatFunction = (str: string) => string; + diff --git a/packages/api/utils/src/formatEnv/types/ReadEnvOptions.i.ts b/packages/api/utils/src/formatEnv/types/ReadEnvOptions.i.ts index c7ff5bf0..cedb645c 100644 --- a/packages/api/utils/src/formatEnv/types/ReadEnvOptions.i.ts +++ b/packages/api/utils/src/formatEnv/types/ReadEnvOptions.i.ts @@ -1,15 +1,20 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Format } from './Format.t'; -import { FormatFunction } from './FormatFunction.t'; - -export interface ReadEnvOptions { - separator: string; - includePrefix: boolean; - format: Format | FormatFunction; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Format } from './Format.t'; +import { FormatFunction } from './FormatFunction.t'; + +/** Read environment options */ +export interface ReadEnvOptions { + /** Environment variable separator */ + separator: string; + /** Include prefix */ + includePrefix: boolean; + /** Format */ + format: Format | FormatFunction; +} + diff --git a/packages/api/utils/src/loadFile/loadFile.ts b/packages/api/utils/src/loadFile/loadFile.ts index 9e905a4a..65130f8e 100644 --- a/packages/api/utils/src/loadFile/loadFile.ts +++ b/packages/api/utils/src/loadFile/loadFile.ts @@ -1,40 +1,43 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import fs from 'fs'; - -type LoggerInstance = { - debug: (message: string) => void; -}; -/** - * Load the file from the path if this exist - * @param path - path to file - * @returns - */ -export function loadFile(path?: string, logger?: LoggerInstance): Buffer | undefined { - const ownLogger = (message: string) => { - if (logger) { - logger.debug(message); - } - }; - if (!path) { - ownLogger('No path provided'); - return undefined; - } else if (path && fs.existsSync(path)) { - try { - return fs.readFileSync(path); - } catch (rawError) { - ownLogger(Crash.from(rawError).message); - return undefined; - } - } else { - // Stryker disable next-line all - ownLogger(`No such file at path ${path}`); - return undefined; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import fs from 'fs'; + +/** Logger instance */ +export type LoggerInstance = { + debug: (message: string) => void; +}; + +/** + * Load the file from the path if this exist + * @param path - path to file + * @returns + */ +export function loadFile(path?: string, logger?: LoggerInstance): Buffer | undefined { + const ownLogger = (message: string) => { + if (logger) { + logger.debug(message); + } + }; + if (!path) { + ownLogger('No path provided'); + return undefined; + } else if (path && fs.existsSync(path)) { + try { + return fs.readFileSync(path); + } catch (rawError) { + ownLogger(Crash.from(rawError).message); + return undefined; + } + } else { + // Stryker disable next-line all + ownLogger(`No such file at path ${path}`); + return undefined; + } +} + diff --git a/packages/components/openc2/README.md b/packages/components/openc2/README.md index cb225493..6b4ba104 100644 --- a/packages/components/openc2/README.md +++ b/packages/components/openc2/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/components/openc2/package.json b/packages/components/openc2/package.json index 166f58ae..13b53b67 100644 --- a/packages/components/openc2/package.json +++ b/packages/components/openc2/package.json @@ -36,18 +36,16 @@ "@mdf.js/redis-provider": "*", "@mdf.js/socket-client-provider": "*", "@mdf.js/socket-server-provider": "*", - "@mdf.js/utils": "*", "jsonwebtoken": "^9.0.0", - "socket.io": "^4.8.0", - "uuid": "^10.0.0" + "socket.io": "^4.8.1", + "uuid": "^11.0.3" }, "devDependencies": { "@mdf.js/repo-config": "*", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.2", - "@types/lodash": "^4.17.10", - "@types/supertest": "^6.0.2", - "@types/uuid": "^10.0.0" + "@types/lodash": "^4.17.13", + "@types/supertest": "^6.0.2" }, "engines": { "node": ">=16.14.2" diff --git a/packages/components/openc2/src/adapters/Dummy/DummyConsumerAdapter.ts b/packages/components/openc2/src/adapters/Dummy/DummyConsumerAdapter.ts index 65538d10..ebe6e8d3 100644 --- a/packages/components/openc2/src/adapters/Dummy/DummyConsumerAdapter.ts +++ b/packages/components/openc2/src/adapters/Dummy/DummyConsumerAdapter.ts @@ -1,37 +1,37 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { ConsumerAdapter } from '@mdf.js/openc2-core'; -import { AdapterOptions } from '../../types'; -import { DummyAdapter } from './DummyAdapter'; - -export class DummyConsumerAdapter extends DummyAdapter implements ConsumerAdapter { - /** - * Create a new OpenC2 adapter for Dummy - * @param adapterOptions - Adapter configuration options - * @param type - component type - */ - constructor(adapterOptions: AdapterOptions) { - super(adapterOptions, 'consumer'); - } - /** - * Subscribe the incoming message handler to the underlayer transport system - * @param handler - handler to be used - * @returns - */ - public async subscribe(handler: any): Promise { - // Dummy adapter does not need to subscribe - } - /** - * Unsubscribe the incoming message handler from the underlayer transport system - * @param handler - handler to be used - * @returns - */ - public async unsubscribe(handler: any): Promise { - // Dummy adapter does not need to unsubscribe - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { ConsumerAdapter } from '@mdf.js/openc2-core'; +import { AdapterOptions } from '../../types'; +import { DummyAdapter } from './DummyAdapter'; + +export class DummyConsumerAdapter extends DummyAdapter implements ConsumerAdapter { + /** + * Create a new OpenC2 adapter for Dummy + * @param adapterOptions - Adapter configuration options + */ + constructor(adapterOptions: AdapterOptions) { + super(adapterOptions, 'consumer'); + } + /** + * Subscribe the incoming message handler to the underlayer transport system + * @param handler - handler to be used + * @returns + */ + public async subscribe(handler: any): Promise { + // Dummy adapter does not need to subscribe + } + /** + * Unsubscribe the incoming message handler from the underlayer transport system + * @param handler - handler to be used + * @returns + */ + public async unsubscribe(handler: any): Promise { + // Dummy adapter does not need to unsubscribe + } +} + diff --git a/packages/components/openc2/src/adapters/Dummy/DummyProducerAdapter.ts b/packages/components/openc2/src/adapters/Dummy/DummyProducerAdapter.ts index 351d34cb..080b7969 100644 --- a/packages/components/openc2/src/adapters/Dummy/DummyProducerAdapter.ts +++ b/packages/components/openc2/src/adapters/Dummy/DummyProducerAdapter.ts @@ -1,31 +1,31 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Control, ProducerAdapter } from '@mdf.js/openc2-core'; -import { AdapterOptions } from '../../types'; -import { DummyAdapter } from './DummyAdapter'; - -export class DummyProducerAdapter extends DummyAdapter implements ProducerAdapter { - /** - * Create a new OpenC2 adapter for Dummy - * @param adapterOptions - Adapter configuration options - * @param type - component type - */ - constructor(adapterOptions: AdapterOptions) { - super(adapterOptions, 'consumer'); - } - /** - * Perform the publication of the message in the underlayer transport system - * @param message - message to be published - * @returns - */ - public async publish( - message: Control.CommandMessage - ): Promise { - // Dummy adapter does not need to publish - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Control, ProducerAdapter } from '@mdf.js/openc2-core'; +import { AdapterOptions } from '../../types'; +import { DummyAdapter } from './DummyAdapter'; + +export class DummyProducerAdapter extends DummyAdapter implements ProducerAdapter { + /** + * Create a new OpenC2 adapter for Dummy + * @param adapterOptions - Adapter configuration options + */ + constructor(adapterOptions: AdapterOptions) { + super(adapterOptions, 'consumer'); + } + /** + * Perform the publication of the message in the underlayer transport system + * @param message - message to be published + * @returns + */ + public async publish( + message: Control.CommandMessage + ): Promise { + // Dummy adapter does not need to publish + } +} + diff --git a/packages/components/openc2/src/adapters/Redis/RedisConsumerAdapter.test.ts b/packages/components/openc2/src/adapters/Redis/RedisConsumerAdapter.test.ts index ef3e7286..523b21be 100644 --- a/packages/components/openc2/src/adapters/Redis/RedisConsumerAdapter.test.ts +++ b/packages/components/openc2/src/adapters/Redis/RedisConsumerAdapter.test.ts @@ -1,332 +1,333 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import { Control } from '@mdf.js/openc2-core'; -import { RedisConsumerAdapter } from './RedisConsumerAdapter'; - -const COMMAND: Control.CommandMessage = { - content_type: 'application/openc2+json;version=1.0', - msg_type: Control.MessageType.Command, - request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', - created: 100, - from: 'myProducer', - to: ['*'], - content: { - action: Control.Action.Query, - target: { - 'x-netin:alarms': { entity: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c' }, - }, - command_id: 'myCommandId', - args: { - duration: 50, - }, - }, -}; -const RESPONSE: Control.ResponseMessage = { - content_type: 'application/openc2+json;version=1.0', - msg_type: Control.MessageType.Response, - request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', - created: 100, - from: 'myConsumer', - to: ['myProducer'], - status: Control.StatusCode.OK, - content: { - status: Control.StatusCode.OK, - results: {}, - }, -}; - -describe('#RedisConsumerAdapter', () => { - describe('#Happy path', () => { - it('Should create a valid instance', () => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - separator: ':', - }); - expect(adapter).toBeInstanceOf(RedisConsumerAdapter); - expect(adapter.name).toEqual('myId'); - //@ts-ignore - Testing private property - expect(adapter.separator).toEqual(':'); - //@ts-ignore - Testing private property - expect(adapter.subscriptions).toEqual([ - 'oc2:cmd:all', - 'oc2:cmd:device:myId', - 'oc2:cmd:ap:actuator1', - ]); - //@ts-ignore - Testing private property - expect(adapter.publisher).toBeDefined(); - //@ts-ignore - Testing private property - expect(adapter.subscriber).toBeDefined(); - const checks = adapter.checks; - expect(checks).toEqual({ - 'myId-publisher:status': [ - { - componentId: checks['myId-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-publisher:status'][0].time, - }, - ], - 'myId-subscriber:status': [ - { - componentId: checks['myId-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-subscriber:status'][0].time, - }, - ], - }); - }, 300); - it('Should start/stop the instance properly', async () => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - expect(adapter).toBeInstanceOf(RedisConsumerAdapter); - expect(adapter.name).toEqual('myId'); - //@ts-ignore - Testing private property - expect(adapter.separator).toEqual('.'); - //@ts-ignore - Testing private property - expect(adapter.subscriptions).toEqual([ - 'oc2.cmd.all', - 'oc2.cmd.device.myId', - 'oc2.cmd.ap.actuator1', - ]); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'start').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber, 'start').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber.client, 'psubscribe').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'stop').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber, 'stop').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber.client, 'punsubscribe').mockResolvedValue(); - const checks = adapter.checks; - expect(checks).toEqual({ - 'myId-publisher:status': [ - { - componentId: checks['myId-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-publisher:status'][0].time, - }, - ], - 'myId-subscriber:status': [ - { - componentId: checks['myId-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-subscriber:status'][0].time, - }, - ], - }); - await adapter.start(); - await adapter.stop(); - }, 300); - it('Should subscribe/unsubcribe the instance properly', async () => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => {}; - //@ts-ignore - Testing private property - expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(0); - //@ts-ignore - Testing private property - expect(adapter.handler).toBeUndefined(); - - await adapter.subscribe(myHandler); - //@ts-ignore - Testing private property - expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(1); - //@ts-ignore - Testing private property - expect(adapter.handler).toEqual(myHandler); - - await adapter.unsubscribe(myHandler); - //@ts-ignore - Testing private property - expect(adapter.handler).toBeUndefined(); - //@ts-ignore - Testing private property - expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(0); - }, 300); - it('Should process incoming messages properly and publish the response FOR ONE DESTINATION', done => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => { - done(undefined, RESPONSE); - }; - adapter.subscribe(myHandler).then(); - jest - //@ts-ignore - Testing private property - .spyOn(adapter.publisher.client, 'publish') - .mockImplementation((topic: string | Buffer, message: string | Buffer, cb?: any) => { - expect(topic).toEqual('oc2.rsp.myProducer'); - expect(message).toEqual(JSON.stringify(RESPONSE)); - done(); - return Promise.resolve(1); - }); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit( - 'pmessage', - 'oc2.cmd.all', - 'oc2.cmd.all', - JSON.stringify(COMMAND) - ); - - adapter.unsubscribe(myHandler).then(); - }, 300); - it('Should process incoming messages properly and publish the response FOR SEVERAL DESTINATIONS', done => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => { - done(undefined, { ...RESPONSE, to: ['*'] }); - }; - adapter.subscribe(myHandler).then(); - jest - //@ts-ignore - Testing private property - .spyOn(adapter.publisher.client, 'publish') - .mockImplementation((topic: string | Buffer, message: string | Buffer, cb?: any) => { - expect(topic).toEqual('oc2.rsp'); - expect(message).toEqual(JSON.stringify({ ...RESPONSE, to: ['*'] })); - done(); - return Promise.resolve(1); - }); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit( - 'pmessage', - 'oc2.cmd.all', - 'oc2.cmd.all', - JSON.stringify(COMMAND) - ); - - adapter.unsubscribe(myHandler).then(); - }, 300); - }); - describe('#Sad path', () => { - it('Should reject if there is a problem starting/stopping', done => { - const adapter = new RedisConsumerAdapter({ id: 'myId' }); - - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'start').mockRejectedValue(new Error('myError')); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'stop').mockRejectedValue(new Error('myError')); - adapter - .start() - .then(() => { - throw new Error('Should not be here'); - }) - .catch(error => { - expect(error.message).toEqual( - 'Error performing the subscription to OpenC2 topics: myError' - ); - }); - adapter - .stop() - .then(() => { - throw new Error('Should not be here'); - }) - .catch(error => { - expect(error.message).toEqual( - 'Error performing the unsubscription to OpenC2 topics: myError' - ); - done(); - }); - }, 300); - it('Should emit an error if there is a problem publishing the response', done => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => { - done(undefined, RESPONSE); - }; - adapter.on('error', error => { - expect(error.message).toEqual('Error performing the publication of the message: myError'); - done(); - }); - adapter.subscribe(myHandler).then(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher.client, 'publish').mockRejectedValue(new Error('myError')); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit( - 'pmessage', - 'oc2.cmd.all', - 'oc2.cmd.all', - JSON.stringify(COMMAND) - ); - }, 300); - it('Should emit an error if the consumer report an error', done => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => { - done(new Error('myError')); - }; - adapter.on('error', error => { - expect(error.message).toEqual('myError'); - done(); - }); - adapter.subscribe(myHandler).then(); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit( - 'pmessage', - 'oc2.cmd.all', - 'oc2.cmd.all', - JSON.stringify(COMMAND) - ); - }, 300); - it('Should emit an error if there is a problem parsing the message', done => { - const adapter = new RedisConsumerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - const myHandler = ( - message: Control.CommandMessage, - done: (error?: Crash | Error, message?: Control.ResponseMessage) => void - ) => {}; - adapter.on('error', error => { - expect(error.message).toEqual( - `Error performing the adaptation of the incoming message: Expected property name or '}' in JSON at position 1` - ); - done(); - }); - adapter.subscribe(myHandler).then(); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit('pmessage', 'oc2.cmd.all', 'oc2.cmd.all', '{'); - }, 300); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { Control } from '@mdf.js/openc2-core'; +import { RedisConsumerAdapter } from './RedisConsumerAdapter'; + +const COMMAND: Control.CommandMessage = { + content_type: 'application/openc2+json;version=1.0', + msg_type: Control.MessageType.Command, + request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', + created: 100, + from: 'myProducer', + to: ['*'], + content: { + action: Control.Action.Query, + target: { + 'x-netin:alarms': { entity: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c' }, + }, + command_id: 'myCommandId', + args: { + duration: 50, + }, + }, +}; +const RESPONSE: Control.ResponseMessage = { + content_type: 'application/openc2+json;version=1.0', + msg_type: Control.MessageType.Response, + request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', + created: 100, + from: 'myConsumer', + to: ['myProducer'], + status: Control.StatusCode.OK, + content: { + status: Control.StatusCode.OK, + results: {}, + }, +}; + +describe('#RedisConsumerAdapter', () => { + describe('#Happy path', () => { + it('Should create a valid instance', () => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + separator: ':', + }); + expect(adapter).toBeInstanceOf(RedisConsumerAdapter); + expect(adapter.name).toEqual('myId'); + //@ts-ignore - Testing private property + expect(adapter.separator).toEqual(':'); + //@ts-ignore - Testing private property + expect(adapter.subscriptions).toEqual([ + 'oc2:cmd:all', + 'oc2:cmd:device:myId', + 'oc2:cmd:ap:actuator1', + ]); + //@ts-ignore - Testing private property + expect(adapter.publisher).toBeDefined(); + //@ts-ignore - Testing private property + expect(adapter.subscriber).toBeDefined(); + const checks = adapter.checks; + expect(checks).toEqual({ + 'myId-publisher:status': [ + { + componentId: checks['myId-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-publisher:status'][0].time, + }, + ], + 'myId-subscriber:status': [ + { + componentId: checks['myId-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-subscriber:status'][0].time, + }, + ], + }); + }, 300); + it('Should start/stop the instance properly', async () => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + expect(adapter).toBeInstanceOf(RedisConsumerAdapter); + expect(adapter.name).toEqual('myId'); + //@ts-ignore - Testing private property + expect(adapter.separator).toEqual('.'); + //@ts-ignore - Testing private property + expect(adapter.subscriptions).toEqual([ + 'oc2.cmd.all', + 'oc2.cmd.device.myId', + 'oc2.cmd.ap.actuator1', + ]); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'start').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber, 'start').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber.client, 'psubscribe').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'stop').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber, 'stop').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber.client, 'punsubscribe').mockResolvedValue(); + const checks = adapter.checks; + expect(checks).toEqual({ + 'myId-publisher:status': [ + { + componentId: checks['myId-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-publisher:status'][0].time, + }, + ], + 'myId-subscriber:status': [ + { + componentId: checks['myId-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-subscriber:status'][0].time, + }, + ], + }); + await adapter.start(); + await adapter.stop(); + }, 300); + it('Should subscribe/unsubcribe the instance properly', async () => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => {}; + //@ts-ignore - Testing private property + expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(0); + //@ts-ignore - Testing private property + expect(adapter.handler).toBeUndefined(); + + await adapter.subscribe(myHandler); + //@ts-ignore - Testing private property + expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(1); + //@ts-ignore - Testing private property + expect(adapter.handler).toEqual(myHandler); + + await adapter.unsubscribe(myHandler); + //@ts-ignore - Testing private property + expect(adapter.handler).toBeUndefined(); + //@ts-ignore - Testing private property + expect(adapter.subscriber.client.listenerCount('pmessage')).toEqual(0); + }, 300); + it('Should process incoming messages properly and publish the response FOR ONE DESTINATION', done => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => { + done(undefined, RESPONSE); + }; + adapter.subscribe(myHandler).then(); + jest + //@ts-ignore - Testing private property + .spyOn(adapter.publisher.client, 'publish') + .mockImplementation((topic: string | Buffer, message: string | Buffer, cb?: any) => { + expect(topic).toEqual('oc2.rsp.myProducer'); + expect(message).toEqual(JSON.stringify(RESPONSE)); + done(); + return Promise.resolve(1); + }); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit( + 'pmessage', + 'oc2.cmd.all', + 'oc2.cmd.all', + JSON.stringify(COMMAND) + ); + + adapter.unsubscribe(myHandler).then(); + }, 300); + it('Should process incoming messages properly and publish the response FOR SEVERAL DESTINATIONS', done => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => { + done(undefined, { ...RESPONSE, to: ['*'] }); + }; + adapter.subscribe(myHandler).then(); + jest + //@ts-ignore - Testing private property + .spyOn(adapter.publisher.client, 'publish') + .mockImplementation((topic: string | Buffer, message: string | Buffer, cb?: any) => { + expect(topic).toEqual('oc2.rsp'); + expect(message).toEqual(JSON.stringify({ ...RESPONSE, to: ['*'] })); + done(); + return Promise.resolve(1); + }); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit( + 'pmessage', + 'oc2.cmd.all', + 'oc2.cmd.all', + JSON.stringify(COMMAND) + ); + + adapter.unsubscribe(myHandler).then(); + }, 300); + }); + describe('#Sad path', () => { + it('Should reject if there is a problem starting/stopping', done => { + const adapter = new RedisConsumerAdapter({ id: 'myId' }); + + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'start').mockRejectedValue(new Error('myError')); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'stop').mockRejectedValue(new Error('myError')); + adapter + .start() + .then(() => { + throw new Error('Should not be here'); + }) + .catch(error => { + expect(error.message).toEqual( + 'Error performing the subscription to OpenC2 topics: myError' + ); + }); + adapter + .stop() + .then(() => { + throw new Error('Should not be here'); + }) + .catch(error => { + expect(error.message).toEqual( + 'Error performing the unsubscription to OpenC2 topics: myError' + ); + done(); + }); + }, 300); + it('Should emit an error if there is a problem publishing the response', done => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => { + done(undefined, RESPONSE); + }; + adapter.on('error', error => { + expect(error.message).toEqual('Error performing the publication of the message: myError'); + done(); + }); + adapter.subscribe(myHandler).then(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher.client, 'publish').mockRejectedValue(new Error('myError')); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit( + 'pmessage', + 'oc2.cmd.all', + 'oc2.cmd.all', + JSON.stringify(COMMAND) + ); + }, 300); + it('Should emit an error if the consumer report an error', done => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => { + done(new Error('myError')); + }; + adapter.on('error', error => { + expect(error.message).toEqual('myError'); + done(); + }); + adapter.subscribe(myHandler).then(); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit( + 'pmessage', + 'oc2.cmd.all', + 'oc2.cmd.all', + JSON.stringify(COMMAND) + ); + }, 300); + it('Should emit an error if there is a problem parsing the message', done => { + const adapter = new RedisConsumerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + const myHandler = ( + message: Control.CommandMessage, + done: (error?: Crash | Error, message?: Control.ResponseMessage) => void + ) => {}; + adapter.on('error', error => { + expect(error.message).toEqual( + `Error performing the adaptation of the incoming message: Expected property name or '}' in JSON at position 1 (line 1 column 2)` + ); + done(); + }); + adapter.subscribe(myHandler).then(); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit('pmessage', 'oc2.cmd.all', 'oc2.cmd.all', '{'); + }, 300); + }); +}); + diff --git a/packages/components/openc2/src/adapters/Redis/RedisProducerAdapter.test.ts b/packages/components/openc2/src/adapters/Redis/RedisProducerAdapter.test.ts index eb12d8ad..3b46f5f1 100644 --- a/packages/components/openc2/src/adapters/Redis/RedisProducerAdapter.test.ts +++ b/packages/components/openc2/src/adapters/Redis/RedisProducerAdapter.test.ts @@ -1,243 +1,244 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Control } from '@mdf.js/openc2-core'; -import { RedisProducerAdapter } from './RedisProducerAdapter'; - -const COMMAND: Control.CommandMessage = { - content_type: 'application/openc2+json;version=1.0', - msg_type: Control.MessageType.Command, - request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', - created: 100, - from: 'myProducer', - to: ['*'], - content: { - action: Control.Action.Query, - target: { - 'x-netin:alarms': { entity: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c' }, - }, - command_id: 'myCommandId', - args: { - duration: 50, - }, - }, -}; -const RESPONSE: Control.ResponseMessage = { - content_type: 'application/openc2+json;version=1.0', - msg_type: Control.MessageType.Response, - request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', - created: 100, - from: 'myConsumer', - to: ['myProducer'], - status: Control.StatusCode.OK, - content: { - status: Control.StatusCode.OK, - results: {}, - }, -}; -describe('#RedisProducerAdapter', () => { - describe('#Happy path', () => { - it('Should create a valid instance', () => { - const adapter = new RedisProducerAdapter({ - id: 'myId', - actuators: ['actuator1'], - separator: ':', - }); - expect(adapter).toBeInstanceOf(RedisProducerAdapter); - expect(adapter.name).toEqual('myId'); - //@ts-ignore - Testing private property - expect(adapter.separator).toEqual(':'); - //@ts-ignore - Testing private property - expect(adapter.subscriptions).toEqual(['oc2:rsp', 'oc2:rsp:myId']); - //@ts-ignore - Testing private property - expect(adapter.publisher).toBeDefined(); - //@ts-ignore - Testing private property - expect(adapter.subscriber).toBeDefined(); - const checks = adapter.checks; - expect(checks).toEqual({ - 'myId-publisher:status': [ - { - componentId: checks['myId-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-publisher:status'][0].time, - }, - ], - 'myId-subscriber:status': [ - { - componentId: checks['myId-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-subscriber:status'][0].time, - }, - ], - }); - }, 300); - it('Should start/stop the instance properly', async () => { - const adapter = new RedisProducerAdapter({ - id: 'myId', - actuators: ['actuator1'], - }); - expect(adapter).toBeInstanceOf(RedisProducerAdapter); - expect(adapter.name).toEqual('myId'); - //@ts-ignore - Testing private property - expect(adapter.separator).toEqual('.'); - //@ts-ignore - Testing private property - expect(adapter.subscriptions).toEqual(['oc2.rsp', 'oc2.rsp.myId']); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'start').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber, 'start').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber.client, 'psubscribe').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'stop').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber, 'stop').mockResolvedValue(); - //@ts-ignore - Testing private property - jest.spyOn(adapter.subscriber.client, 'punsubscribe').mockResolvedValue(); - const checks = adapter.checks; - expect(checks).toEqual({ - 'myId-publisher:status': [ - { - componentId: checks['myId-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-publisher:status'][0].time, - }, - ], - 'myId-subscriber:status': [ - { - componentId: checks['myId-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['myId-subscriber:status'][0].time, - }, - ], - }); - await adapter.start(); - await adapter.stop(); - }, 300); - it('Should publish message properly to all the nodes', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - jest - //@ts-ignore - Testing private property - .spyOn(adapter.publisher.client, 'publish') - .mockImplementation((channel: string | Buffer, message: string | Buffer) => { - expect(channel).toEqual('oc2.cmd.all'); - expect(message).toEqual(JSON.stringify(COMMAND)); - done(); - return Promise.resolve(1); - }); - adapter.publish(COMMAND).then(); - }, 300); - it('Should publish message properly tos single node and actuators', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - let messages = 0; - jest - //@ts-ignore - Testing private property - .spyOn(adapter.publisher.client, 'publish') - .mockImplementation((channel: string | Buffer, message: string | Buffer) => { - messages++; - expect( - ['oc2.cmd.ap.myActuator', 'oc2.cmd.device.actuator1'].includes(channel as string) - ).toBeTruthy(); - expect(message).toEqual( - JSON.stringify({ - ...COMMAND, - to: ['actuator1'], - content: { ...COMMAND.content, actuator: { myActuator: {} } }, - }) - ); - if (messages === 2) { - done(); - } - return Promise.resolve(1); - }); - adapter - .publish({ - ...COMMAND, - to: ['actuator1'], - content: { ...COMMAND.content, actuator: { myActuator: {} } }, - }) - .then(); - }, 300); - it('Should process incoming responses properly', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - adapter.on(RESPONSE.request_id, (response: Control.ResponseMessage) => { - expect(response).toEqual(RESPONSE); - done(); - }); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit('pmessage', 'oc2.rsp', 'oc2.rsp', JSON.stringify(RESPONSE)); - }, 300); - }); - describe('#Sad path', () => { - it('Should throw an error if there is a problem starting/stopping the instance', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'start').mockRejectedValue(new Error('myError')); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher, 'stop').mockRejectedValue(new Error('myError')); - adapter - .start() - .then(() => { - throw new Error('Should not be here'); - }) - .catch(error => { - expect(error.message).toEqual( - 'Error performing the subscription to OpenC2 topics: myError' - ); - }); - adapter - .stop() - .then(() => { - throw new Error('Should not be here'); - }) - .catch(error => { - expect(error.message).toEqual( - 'Error performing the unsubscription to OpenC2 topics: myError' - ); - done(); - }); - }, 300); - it('Should rejects if there is a problem publishing a message', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - //@ts-ignore - Testing private property - jest.spyOn(adapter.publisher.client, 'publish').mockRejectedValue(new Error('myError')); - adapter - .publish(COMMAND) - .then(() => { - throw new Error('Should not be here'); - }) - .catch(error => { - expect(error.message).toEqual('Error performing the publication of the message: myError'); - done(); - }); - }, 300); - it('Should emit an error if there is a problem processing incoming responses', done => { - const adapter = new RedisProducerAdapter({ id: 'myId' }); - adapter.on('error', error => { - expect(error.message).toEqual( - `Error performing the adaptation of the incoming message: Expected property name or '}' in JSON at position 1` - ); - done(); - }); - //@ts-ignore - Testing private property - adapter.subscriber.client.emit('pmessage', 'oc2.rsp', 'oc2.rsp', '{'); - }, 300); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Control } from '@mdf.js/openc2-core'; +import { RedisProducerAdapter } from './RedisProducerAdapter'; + +const COMMAND: Control.CommandMessage = { + content_type: 'application/openc2+json;version=1.0', + msg_type: Control.MessageType.Command, + request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', + created: 100, + from: 'myProducer', + to: ['*'], + content: { + action: Control.Action.Query, + target: { + 'x-netin:alarms': { entity: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c' }, + }, + command_id: 'myCommandId', + args: { + duration: 50, + }, + }, +}; +const RESPONSE: Control.ResponseMessage = { + content_type: 'application/openc2+json;version=1.0', + msg_type: Control.MessageType.Response, + request_id: '3b6771cb-1ca6-4c1f-a06e-0b413872cd5c', + created: 100, + from: 'myConsumer', + to: ['myProducer'], + status: Control.StatusCode.OK, + content: { + status: Control.StatusCode.OK, + results: {}, + }, +}; +describe('#RedisProducerAdapter', () => { + describe('#Happy path', () => { + it('Should create a valid instance', () => { + const adapter = new RedisProducerAdapter({ + id: 'myId', + actuators: ['actuator1'], + separator: ':', + }); + expect(adapter).toBeInstanceOf(RedisProducerAdapter); + expect(adapter.name).toEqual('myId'); + //@ts-ignore - Testing private property + expect(adapter.separator).toEqual(':'); + //@ts-ignore - Testing private property + expect(adapter.subscriptions).toEqual(['oc2:rsp', 'oc2:rsp:myId']); + //@ts-ignore - Testing private property + expect(adapter.publisher).toBeDefined(); + //@ts-ignore - Testing private property + expect(adapter.subscriber).toBeDefined(); + const checks = adapter.checks; + expect(checks).toEqual({ + 'myId-publisher:status': [ + { + componentId: checks['myId-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-publisher:status'][0].time, + }, + ], + 'myId-subscriber:status': [ + { + componentId: checks['myId-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-subscriber:status'][0].time, + }, + ], + }); + }, 300); + it('Should start/stop the instance properly', async () => { + const adapter = new RedisProducerAdapter({ + id: 'myId', + actuators: ['actuator1'], + }); + expect(adapter).toBeInstanceOf(RedisProducerAdapter); + expect(adapter.name).toEqual('myId'); + //@ts-ignore - Testing private property + expect(adapter.separator).toEqual('.'); + //@ts-ignore - Testing private property + expect(adapter.subscriptions).toEqual(['oc2.rsp', 'oc2.rsp.myId']); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'start').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber, 'start').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber.client, 'psubscribe').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'stop').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber, 'stop').mockResolvedValue(); + //@ts-ignore - Testing private property + jest.spyOn(adapter.subscriber.client, 'punsubscribe').mockResolvedValue(); + const checks = adapter.checks; + expect(checks).toEqual({ + 'myId-publisher:status': [ + { + componentId: checks['myId-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-publisher:status'][0].time, + }, + ], + 'myId-subscriber:status': [ + { + componentId: checks['myId-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['myId-subscriber:status'][0].time, + }, + ], + }); + await adapter.start(); + await adapter.stop(); + }, 300); + it('Should publish message properly to all the nodes', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + jest + //@ts-ignore - Testing private property + .spyOn(adapter.publisher.client, 'publish') + .mockImplementation((channel: string | Buffer, message: string | Buffer) => { + expect(channel).toEqual('oc2.cmd.all'); + expect(message).toEqual(JSON.stringify(COMMAND)); + done(); + return Promise.resolve(1); + }); + adapter.publish(COMMAND).then(); + }, 300); + it('Should publish message properly tos single node and actuators', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + let messages = 0; + jest + //@ts-ignore - Testing private property + .spyOn(adapter.publisher.client, 'publish') + .mockImplementation((channel: string | Buffer, message: string | Buffer) => { + messages++; + expect( + ['oc2.cmd.ap.myActuator', 'oc2.cmd.device.actuator1'].includes(channel as string) + ).toBeTruthy(); + expect(message).toEqual( + JSON.stringify({ + ...COMMAND, + to: ['actuator1'], + content: { ...COMMAND.content, actuator: { myActuator: {} } }, + }) + ); + if (messages === 2) { + done(); + } + return Promise.resolve(1); + }); + adapter + .publish({ + ...COMMAND, + to: ['actuator1'], + content: { ...COMMAND.content, actuator: { myActuator: {} } }, + }) + .then(); + }, 300); + it('Should process incoming responses properly', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + adapter.on(RESPONSE.request_id, (response: Control.ResponseMessage) => { + expect(response).toEqual(RESPONSE); + done(); + }); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit('pmessage', 'oc2.rsp', 'oc2.rsp', JSON.stringify(RESPONSE)); + }, 300); + }); + describe('#Sad path', () => { + it('Should throw an error if there is a problem starting/stopping the instance', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'start').mockRejectedValue(new Error('myError')); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher, 'stop').mockRejectedValue(new Error('myError')); + adapter + .start() + .then(() => { + throw new Error('Should not be here'); + }) + .catch(error => { + expect(error.message).toEqual( + 'Error performing the subscription to OpenC2 topics: myError' + ); + }); + adapter + .stop() + .then(() => { + throw new Error('Should not be here'); + }) + .catch(error => { + expect(error.message).toEqual( + 'Error performing the unsubscription to OpenC2 topics: myError' + ); + done(); + }); + }, 300); + it('Should rejects if there is a problem publishing a message', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + //@ts-ignore - Testing private property + jest.spyOn(adapter.publisher.client, 'publish').mockRejectedValue(new Error('myError')); + adapter + .publish(COMMAND) + .then(() => { + throw new Error('Should not be here'); + }) + .catch(error => { + expect(error.message).toEqual('Error performing the publication of the message: myError'); + done(); + }); + }, 300); + it('Should emit an error if there is a problem processing incoming responses', done => { + const adapter = new RedisProducerAdapter({ id: 'myId' }); + adapter.on('error', error => { + expect(error.message).toEqual( + `Error performing the adaptation of the incoming message: Expected property name or '}' in JSON at position 1 (line 1 column 2)` + ); + done(); + }); + //@ts-ignore - Testing private property + adapter.subscriber.client.emit('pmessage', 'oc2.rsp', 'oc2.rsp', '{'); + }, 300); + }); +}); + diff --git a/packages/components/openc2/src/index.ts b/packages/components/openc2/src/index.ts index d3690f2f..6d9f6af8 100644 --- a/packages/components/openc2/src/index.ts +++ b/packages/components/openc2/src/index.ts @@ -1,25 +1,31 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { - CommandJobDone, - CommandJobHandler, - Consumer, - ConsumerOptions, - Control, - Gateway, - GatewayOptions, - Producer, - ProducerOptions, - Registry, - Resolver, - ResolverEntry, - ResolverMap, -} from '@mdf.js/openc2-core'; -export * as Adapters from './adapters'; -export * as Factory from './factories'; -export { ServiceBus, ServiceBusOptions } from './serviceBus'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { + CommandJobDone, + CommandJobHandler, + Consumer, + ConsumerOptions, + Control, + Gateway, + GatewayOptions, + Producer, + ProducerOptions, + Registry, + Resolver, + ResolverEntry, + ResolverMap, +} from '@mdf.js/openc2-core'; +export * as Adapters from './adapters'; +export * as Factory from './factories'; +export { ServiceBus, ServiceBusOptions } from './serviceBus'; +export { + AdapterOptions, + RedisClientOptions, + SocketIOClientOptions, + SocketIOServerOptions, +} from './types'; diff --git a/packages/components/openc2/src/serviceBus/ServiceBus.ts b/packages/components/openc2/src/serviceBus/ServiceBus.ts index f444a870..df70b8c2 100644 --- a/packages/components/openc2/src/serviceBus/ServiceBus.ts +++ b/packages/components/openc2/src/serviceBus/ServiceBus.ts @@ -1,256 +1,257 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Health, Layer } from '@mdf.js/core'; -import { overallStatus } from '@mdf.js/core/dist/Health'; -import { Crash } from '@mdf.js/crash'; -import { Accessors, Control } from '@mdf.js/openc2-core'; -import { SocketIOServer } from '@mdf.js/socket-server-provider'; -import EventEmitter from 'events'; -import os from 'os'; -import { Namespace, Socket } from 'socket.io'; -import { v4 } from 'uuid'; -import { SocketIOServerOptions } from '../types'; -import { AddressMapper } from './AddressMapper'; -import { Events } from './Events'; -import { AuthZ, Check } from './middlewares'; - -const ALLOWED_DISCONNECT_REASONS = [ - 'server namespace disconnect', - 'client namespace disconnect', - 'server shutting down', -]; - -const SUBJECT = 'Socket.IO OpenC2'; - -export declare interface ServiceBus { - /** Emitted when a server operation has some problem */ - on(event: 'error', listener: (error: Crash | Error) => void): this; - /** Emitted on every state change */ - on(event: 'status', listener: (status: Health.Status) => void): this; -} - -export interface ServiceBusOptions { - /** Define the use of JWT tokens for client authentication */ - useJwt?: boolean; - /** Secret used in JWT token validation */ - secret?: string; -} -export class ServiceBus extends EventEmitter implements Layer.App.Resource { - /** Component identification */ - public readonly componentId: string = v4(); - /** Socket.IO server instance */ - private readonly instance: SocketIOServer.Provider; - /** OpenC2 Namespace */ - private readonly oc2Namespace: Namespace; - /** Address mapper */ - private readonly addressMapper: AddressMapper; - /** - * Create a new ServiceBus instance - * @param serverOptions - Socket.IO server options - * @param options - Socket.IO client configuration options - * @param name - name of the service bus - */ - constructor( - serverOptions: SocketIOServerOptions, - options: ServiceBusOptions, - public readonly name: string - ) { - super(); - this.addressMapper = new AddressMapper(); - this.instance = SocketIOServer.Factory.create({ - config: { ...serverOptions, transports: ['websocket'] }, - name, - }); - this.oc2Namespace = this.instance.client.of('/openc2'); - this.oc2Namespace.use(Check.handler()); - if (options.useJwt) { - this.oc2Namespace.use(AuthZ.handler({ secret: options.secret })); - } - this.oc2Namespace.on('connection', this.onConnectionEventOC2Namespace); - this.instance.on('error', this.onErrorHandler); - this.instance.on('status', this.onStatusHandler); - } - /** - * Connection event handler for the OpenC2 namespace - * @param socket - socket to be configured - */ - private readonly onConnectionEventOC2Namespace = (socket: Socket): void => { - this.addressMapper.update(socket.id, socket.handshake.auth['nodeId']); - if (socket.handshake.auth['type'] === 'producer') { - socket.join('producer'); - } else if (socket.handshake.auth['type'] === 'consumer') { - socket.join('consumer'); - for (const actuator of socket.handshake.auth['actuators']) { - socket.join(actuator); - } - } - socket.onAny(this.eventHandler); - socket.on('disconnect', this.onDisconnectEvent); - }; - /** - * Manage the incoming commands events from provider - * @param event - event name - * @param command - command message from provider - * @param callback - callback function, used as acknowledgement - */ - private readonly eventHandler = ( - event: string, - command: Control.CommandMessage, - callback: (responses: Control.ResponseMessage[]) => void - ) => { - const wrappedCallback = (error: Error | null, responses: Control.ResponseMessage[]) => { - if (error) { - this.onErrorHandler( - new Crash( - `Error in the acknowledgement callback function: ${error.message}`, - this.componentId, - { info: { event, command, subject: SUBJECT } } - ) - ); - } else if (!responses || (Array.isArray(responses) && responses.length === 0)) { - this.onErrorHandler( - new Crash( - 'No responses returned in the acknowledgement callback function', - this.componentId, - { info: { event, command, subject: SUBJECT } } - ) - ); - } else { - callback(responses); - } - }; - // Command to oc2/cmd/all is sent to all consumers - if (Events.isGeneralCommandEvent(event)) { - this.oc2Namespace - .in('consumer') - .timeout(Accessors.getDelayFromCommandMessage(command)) - .emit(event, command, wrappedCallback); - } - // Command to oc2/cmd/ap/[actuator_profile] is sent to consumers that have this profile - else if (Events.isActuatorCommandEvent(event)) { - this.oc2Namespace - .in(Events.getActuatorFromCommandEvent(event)) - .timeout(Accessors.getDelayFromCommandMessage(command)) - .emit(event, command, wrappedCallback); - } - // Command to oc2/cmd/device/[device_id] is sent to the consumer that has this id - else if (Events.isDeviceCommandEvent(event)) { - const openC2Id = Events.getDeviceFromCommandEvent(event); - const socketId = this.addressMapper.getByOpenC2Id(openC2Id); - if (socketId) { - this.oc2Namespace - .to(socketId) - .timeout(Accessors.getDelayFromCommandMessage(command)) - .emit(event, command, wrappedCallback); - } - } else { - this.onErrorHandler( - new Crash( - `Invalid command from or message in OpenC2 Socket.IO Server: ${event}`, - this.componentId, - { info: { event, command, subject: SUBJECT } } - ) - ); - } - }; - /** Socket disconnection handler */ - private readonly onDisconnectEvent = (socketId: string): ((reason: string) => void) => { - return (reason: string) => { - const openC2Id = this.addressMapper.getBySocketId(socketId); - const error = this.disconnectReasonToCrashError(reason, openC2Id || 'unknown'); - this.addressMapper.delete(socketId); - if (error) { - this.onErrorHandler(error); - } - }; - }; - /** - * Transforms the disconnection reason in a Crash if its an unmanaged reason - * @param reason - reason for the error - * @param openC2Id - openC2 identification - * @returns - */ - private disconnectReasonToCrashError(reason: string, openC2Id: string): Crash | undefined { - if (!ALLOWED_DISCONNECT_REASONS.includes(reason)) { - return new Crash( - `OpenC2 node ${openC2Id} has been disconnected due to: ${reason}`, - this.componentId, - { info: { openC2Id: openC2Id, subject: SUBJECT } } - ); - } else { - return undefined; - } - } - /** - * Manage the error in the service bus - * @param error - error to be processed - */ - private readonly onErrorHandler = (error: unknown) => { - const crash = Crash.from(error); - if (this.listenerCount('error') > 0) { - this.emit('error', crash); - } - }; - /** - * Manage the status change in the service bus - * @param status - status to be processed - */ - private readonly onStatusHandler = (status: Health.Status): void => { - if (this.listenerCount('status') > 0) { - this.emit('status', status); - } - }; - /** Start the underlayer Socket.IO server */ - public start(): Promise { - return this.instance.start(); - } - /** Close the server and disconnect all the actual connections */ - public stop(): Promise { - this.instance.client.disconnectSockets(); - return this.instance.stop(); - } - /** Close the server and disconnect all the actual connections */ - public close(): Promise { - this.instance.client.disconnectSockets(); - return this.instance.close(); - } - /** Return the status of the server */ - public get status(): Health.Status { - return overallStatus(this.checks); - } - /** - * Return the status of the server in a standard format - * @returns _check object_ as defined in the draft standard - * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 - */ - public get checks(): Health.Checks { - return { - ...this.instance.checks, - [`${this.name}:serverStats`]: [ - { - componentId: this.componentId, - componentName: this.name, - componentType: 'server', - observedValue: { - hostname: os.hostname(), - pid: process.pid, - uptime: process.uptime(), - clientsCount: this.instance.client.engine.clientsCount, - pollingClientsCount: this.instance.client._pollingClientsCount, - namespaces: Array.from(this.instance.client._nsps.values()).map(nsp => ({ - name: nsp.name, - socketsCount: nsp.sockets.size, - })), - }, - observedUnit: 'stats', - status: 'pass', - time: new Date().toISOString(), - }, - ], - }; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Health, Layer } from '@mdf.js/core'; +import { overallStatus } from '@mdf.js/core/dist/Health'; +import { Crash } from '@mdf.js/crash'; +import { Accessors, Control } from '@mdf.js/openc2-core'; +import { SocketIOServer } from '@mdf.js/socket-server-provider'; +import EventEmitter from 'events'; +import os from 'os'; +import { Namespace, Socket } from 'socket.io'; +import { v4 } from 'uuid'; +import { SocketIOServerOptions } from '../types'; +import { AddressMapper } from './AddressMapper'; +import { Events } from './Events'; +import { AuthZ, Check } from './middlewares'; + +const ALLOWED_DISCONNECT_REASONS = [ + 'server namespace disconnect', + 'client namespace disconnect', + 'server shutting down', +]; + +const SUBJECT = 'Socket.IO OpenC2'; + +export declare interface ServiceBus { + /** Emitted when a server operation has some problem */ + on(event: 'error', listener: (error: Crash | Error) => void): this; + /** Emitted on every state change */ + on(event: 'status', listener: (status: Health.Status) => void): this; +} + +export interface ServiceBusOptions { + /** Define the use of JWT tokens for client authentication */ + useJwt?: boolean; + /** Secret used in JWT token validation */ + secret?: string; +} +export class ServiceBus extends EventEmitter implements Layer.App.Resource { + /** Component identification */ + public readonly componentId: string = v4(); + /** Socket.IO server instance */ + private readonly instance: SocketIOServer.Provider; + /** OpenC2 Namespace */ + private readonly oc2Namespace: Namespace; + /** Address mapper */ + private readonly addressMapper: AddressMapper; + /** + * Create a new ServiceBus instance + * @param serverOptions - Socket.IO server options + * @param options - Socket.IO client configuration options + * @param name - name of the service bus + */ + constructor( + serverOptions: SocketIOServerOptions, + options: ServiceBusOptions, + public readonly name: string + ) { + super(); + this.addressMapper = new AddressMapper(); + this.instance = SocketIOServer.Factory.create({ + config: { ...serverOptions, transports: ['websocket'] }, + name, + }); + this.oc2Namespace = this.instance.client.of('/openc2'); + this.oc2Namespace.use(Check.handler()); + if (options.useJwt) { + this.oc2Namespace.use(AuthZ.handler({ secret: options.secret })); + } + this.oc2Namespace.on('connection', this.onConnectionEventOC2Namespace); + this.instance.on('error', this.onErrorHandler); + this.instance.on('status', this.onStatusHandler); + } + /** + * Connection event handler for the OpenC2 namespace + * @param socket - socket to be configured + */ + private readonly onConnectionEventOC2Namespace = (socket: Socket): void => { + this.addressMapper.update(socket.id, socket.handshake.auth['nodeId']); + if (socket.handshake.auth['type'] === 'producer') { + socket.join('producer'); + } else if (socket.handshake.auth['type'] === 'consumer') { + socket.join('consumer'); + for (const actuator of socket.handshake.auth['actuators']) { + socket.join(actuator); + } + } + socket.onAny(this.eventHandler); + socket.on('disconnect', this.onDisconnectEvent); + }; + /** + * Manage the incoming commands events from provider + * @param event - event name + * @param command - command message from provider + * @param callback - callback function, used as acknowledgement + */ + private readonly eventHandler = ( + event: string, + command: Control.CommandMessage, + callback: (responses: Control.ResponseMessage[]) => void + ) => { + const wrappedCallback = (error: Error | null, responses: Control.ResponseMessage[]) => { + if (error) { + this.onErrorHandler( + new Crash( + `Error in the acknowledgement callback function: ${error.message}`, + this.componentId, + { info: { event, command, subject: SUBJECT } } + ) + ); + } else if (!responses || (Array.isArray(responses) && responses.length === 0)) { + this.onErrorHandler( + new Crash( + 'No responses returned in the acknowledgement callback function', + this.componentId, + { info: { event, command, subject: SUBJECT } } + ) + ); + } else { + callback(responses); + } + }; + // Command to oc2/cmd/all is sent to all consumers + if (Events.isGeneralCommandEvent(event)) { + this.oc2Namespace + .in('consumer') + .timeout(Accessors.getDelayFromCommandMessage(command)) + .emit(event, command, wrappedCallback); + } + // Command to oc2/cmd/ap/[actuator_profile] is sent to consumers that have this profile + else if (Events.isActuatorCommandEvent(event)) { + this.oc2Namespace + .in(Events.getActuatorFromCommandEvent(event)) + .timeout(Accessors.getDelayFromCommandMessage(command)) + .emit(event, command, wrappedCallback); + } + // Command to oc2/cmd/device/[device_id] is sent to the consumer that has this id + else if (Events.isDeviceCommandEvent(event)) { + const openC2Id = Events.getDeviceFromCommandEvent(event); + const socketId = this.addressMapper.getByOpenC2Id(openC2Id); + if (socketId) { + this.oc2Namespace + .to(socketId) + .timeout(Accessors.getDelayFromCommandMessage(command)) + .emit(event, command, wrappedCallback); + } + } else { + this.onErrorHandler( + new Crash( + `Invalid command from or message in OpenC2 Socket.IO Server: ${event}`, + this.componentId, + { info: { event, command, subject: SUBJECT } } + ) + ); + } + }; + /** Socket disconnection handler */ + private readonly onDisconnectEvent = (socketId: string): ((reason: string) => void) => { + return (reason: string) => { + const openC2Id = this.addressMapper.getBySocketId(socketId); + const error = this.disconnectReasonToCrashError(reason, openC2Id ?? 'unknown'); + this.addressMapper.delete(socketId); + if (error) { + this.onErrorHandler(error); + } + }; + }; + /** + * Transforms the disconnection reason in a Crash if its an unmanaged reason + * @param reason - reason for the error + * @param openC2Id - openC2 identification + * @returns + */ + private disconnectReasonToCrashError(reason: string, openC2Id: string): Crash | undefined { + if (!ALLOWED_DISCONNECT_REASONS.includes(reason)) { + return new Crash( + `OpenC2 node ${openC2Id} has been disconnected due to: ${reason}`, + this.componentId, + { info: { openC2Id: openC2Id, subject: SUBJECT } } + ); + } else { + return undefined; + } + } + /** + * Manage the error in the service bus + * @param error - error to be processed + */ + private readonly onErrorHandler = (error: unknown) => { + const crash = Crash.from(error); + if (this.listenerCount('error') > 0) { + this.emit('error', crash); + } + }; + /** + * Manage the status change in the service bus + * @param status - status to be processed + */ + private readonly onStatusHandler = (status: Health.Status): void => { + if (this.listenerCount('status') > 0) { + this.emit('status', status); + } + }; + /** Start the underlayer Socket.IO server */ + public start(): Promise { + return this.instance.start(); + } + /** Close the server and disconnect all the actual connections */ + public stop(): Promise { + this.instance.client.disconnectSockets(); + return this.instance.stop(); + } + /** Close the server and disconnect all the actual connections */ + public close(): Promise { + this.instance.client.disconnectSockets(); + return this.instance.close(); + } + /** Return the status of the server */ + public get status(): Health.Status { + return overallStatus(this.checks); + } + /** + * Return the status of the server in a standard format + * @returns _check object_ as defined in the draft standard + * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 + */ + public get checks(): Health.Checks { + return { + ...this.instance.checks, + [`${this.name}:serverStats`]: [ + { + componentId: this.componentId, + componentName: this.name, + componentType: 'server', + observedValue: { + hostname: os.hostname(), + pid: process.pid, + uptime: process.uptime(), + clientsCount: this.instance.client.engine.clientsCount, + pollingClientsCount: this.instance.client._pollingClientsCount, + namespaces: Array.from(this.instance.client._nsps.values()).map(nsp => ({ + name: nsp.name, + socketsCount: nsp.sockets.size, + })), + }, + observedUnit: 'stats', + status: 'pass', + time: new Date().toISOString(), + }, + ], + }; + } +} + diff --git a/packages/components/openc2/src/serviceBus/middlewares/authz/authz.ts b/packages/components/openc2/src/serviceBus/middlewares/authz/authz.ts index 82183aaf..f5a5d066 100644 --- a/packages/components/openc2/src/serviceBus/middlewares/authz/authz.ts +++ b/packages/components/openc2/src/serviceBus/middlewares/authz/authz.ts @@ -1,102 +1,103 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { BoomHelpers, Crash } from '@mdf.js/crash'; -import jwt, { Algorithm, JwtPayload } from 'jsonwebtoken'; -import { Socket } from 'socket.io'; -import { v4 } from 'uuid'; -import { SocketIOMiddleware, SocketIONextFunction, transformError } from '..'; - -const DEFAULT_CONFIG_JWT_TOKEN_SECRET = v4(); -const DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS: Algorithm[] = ['HS256']; -const DEFAULT_CONFIG_JWT_ON_AUTHORIZATION = () => Promise.resolve(); - -/** Options for configuring authorization middleware */ -export type AuthZOptions = { - /** The secret used to sign and verify JWT tokens */ - secret?: string; - /** The algorithms used for JWT token verification */ - algorithms?: Algorithm[]; - /** - * A callback function called when authorization is successful. - * @param decodedToken - The decoded JWT payload. - */ - onAuthorization?: (decodedToken: JwtPayload) => Promise; -}; -/** - * Check the token of the request - * @param options - authorization options - * @returns - */ -function authZ(options: AuthZOptions = {}): SocketIOMiddleware { - return (socket: Socket, next: SocketIONextFunction) => { - const token = socket.handshake.auth['token']; - const secret = options.secret || DEFAULT_CONFIG_JWT_TOKEN_SECRET; - const algorithms = options.algorithms || DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS; - const onAuthorization = options.onAuthorization || DEFAULT_CONFIG_JWT_ON_AUTHORIZATION; - const requestId = v4(); - - hasValidAuthenticationInformation(token, requestId) - .then(reportedToken => verify(reportedToken, { secret, algorithms }, requestId)) - .then(onAuthorization) - .then(() => next()) - .catch(error => next(transformError(error))); - }; -} -/** - * Check if the token is present and an string - * @param token - token to checked - * @param uuid - request identifier - * @returns - */ -function hasValidAuthenticationInformation(token: string, uuid: string): Promise { - return new Promise((resolve, reject) => { - if (!token) { - reject(BoomHelpers.badRequest(`No present authorization information`, uuid)); - } else if (typeof token !== 'string') { - reject(BoomHelpers.badRequest(`Malformed authorization information`, uuid)); - } else { - resolve(token); - } - }); -} -/** - * Check if the token is a valid token - * @param token - token to be verified - * @param options - authorization options - * @param uuid - request identifier - * @returns - */ -function verify( - token: string, - options: Required>, - uuid: string -): Promise { - return new Promise((resolve, reject) => { - jwt.verify(token, options.secret, { algorithms: options.algorithms }, (error, decoded) => { - if (error) { - reject(BoomHelpers.unauthorized(`No valid token: ${error.message}`, uuid)); - } else if (!decoded) { - reject(new Crash(`Error verifying the JWT token`, uuid)); - } else if (typeof decoded === 'string') { - reject(BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid)); - } else { - resolve(decoded); - } - }); - }); -} -/** AuthZ */ -export class AuthZ { - /** - * Perform the authorization based on jwt token for Socket.IO - * @param options - authorization options - * @returns - */ - public static handler(options?: AuthZOptions): SocketIOMiddleware { - return authZ(options); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { BoomHelpers, Crash } from '@mdf.js/crash'; +import jwt, { Algorithm, JwtPayload } from 'jsonwebtoken'; +import { Socket } from 'socket.io'; +import { v4 } from 'uuid'; +import { SocketIOMiddleware, SocketIONextFunction, transformError } from '..'; + +const DEFAULT_CONFIG_JWT_TOKEN_SECRET = v4(); +const DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS: Algorithm[] = ['HS256']; +const DEFAULT_CONFIG_JWT_ON_AUTHORIZATION = () => Promise.resolve(); + +/** Options for configuring authorization middleware */ +export type AuthZOptions = { + /** The secret used to sign and verify JWT tokens */ + secret?: string; + /** The algorithms used for JWT token verification */ + algorithms?: Algorithm[]; + /** + * A callback function called when authorization is successful. + * @param decodedToken - The decoded JWT payload. + */ + onAuthorization?: (decodedToken: JwtPayload) => Promise; +}; +/** + * Check the token of the request + * @param options - authorization options + * @returns + */ +function authZ(options: AuthZOptions = {}): SocketIOMiddleware { + return (socket: Socket, next: SocketIONextFunction) => { + const token = socket.handshake.auth['token']; + const secret = options.secret ?? DEFAULT_CONFIG_JWT_TOKEN_SECRET; + const algorithms = options.algorithms ?? DEFAULT_CONFIG_JWT_TOKEN_ALGORITHMS; + const onAuthorization = options.onAuthorization ?? DEFAULT_CONFIG_JWT_ON_AUTHORIZATION; + const requestId = v4(); + + hasValidAuthenticationInformation(token, requestId) + .then(reportedToken => verify(reportedToken, { secret, algorithms }, requestId)) + .then(onAuthorization) + .then(() => next()) + .catch(error => next(transformError(error))); + }; +} +/** + * Check if the token is present and an string + * @param token - token to checked + * @param uuid - request identifier + * @returns + */ +function hasValidAuthenticationInformation(token: string, uuid: string): Promise { + return new Promise((resolve, reject) => { + if (!token) { + reject(BoomHelpers.badRequest(`No present authorization information`, uuid)); + } else if (typeof token !== 'string') { + reject(BoomHelpers.badRequest(`Malformed authorization information`, uuid)); + } else { + resolve(token); + } + }); +} +/** + * Check if the token is a valid token + * @param token - token to be verified + * @param options - authorization options + * @param uuid - request identifier + * @returns + */ +function verify( + token: string, + options: Required>, + uuid: string +): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, options.secret, { algorithms: options.algorithms }, (error, decoded) => { + if (error) { + reject(BoomHelpers.unauthorized(`No valid token: ${error.message}`, uuid)); + } else if (!decoded) { + reject(new Crash(`Error verifying the JWT token`, uuid)); + } else if (typeof decoded === 'string') { + reject(BoomHelpers.badRequest(`Malformed request, malformed authorization token`, uuid)); + } else { + resolve(decoded); + } + }); + }); +} +/** AuthZ */ +export class AuthZ { + /** + * Perform the authorization based on jwt token for Socket.IO + * @param options - authorization options + * @returns + */ + public static handler(options?: AuthZOptions): SocketIOMiddleware { + return authZ(options); + } +} + diff --git a/packages/components/openc2/src/types/AdapterOptions.i.ts b/packages/components/openc2/src/types/AdapterOptions.i.ts index 50ac8029..05a7debf 100644 --- a/packages/components/openc2/src/types/AdapterOptions.i.ts +++ b/packages/components/openc2/src/types/AdapterOptions.i.ts @@ -1,17 +1,19 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export interface AdapterOptions { - /** Instance identification */ - id: string; - /** Channel scope separator */ - separator?: string; - /** Actuators */ - actuators?: string[]; - /** Authorization token */ - token?: string; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Adapter options */ +export interface AdapterOptions { + /** Instance identification */ + id: string; + /** Channel scope separator */ + separator?: string; + /** Actuators */ + actuators?: string[]; + /** Authorization token */ + token?: string; +} + diff --git a/packages/components/openc2/src/types/ProviderConfigOptions.t.ts b/packages/components/openc2/src/types/ProviderConfigOptions.t.ts index 6e458543..ba432bbf 100644 --- a/packages/components/openc2/src/types/ProviderConfigOptions.t.ts +++ b/packages/components/openc2/src/types/ProviderConfigOptions.t.ts @@ -1,14 +1,18 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Redis } from '@mdf.js/redis-provider'; -import { SocketIOClient } from '@mdf.js/socket-client-provider'; -import { SocketIOServer } from '@mdf.js/socket-server-provider'; - -export type RedisClientOptions = Redis.Config; -export type SocketIOClientOptions = SocketIOClient.Config; -export type SocketIOServerOptions = SocketIOServer.Config; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Redis } from '@mdf.js/redis-provider'; +import { SocketIOClient } from '@mdf.js/socket-client-provider'; +import { SocketIOServer } from '@mdf.js/socket-server-provider'; + +/** Redis client options */ +export type RedisClientOptions = Redis.Config; +/** SocketIO client options */ +export type SocketIOClientOptions = SocketIOClient.Config; +/** SocketIO server options */ +export type SocketIOServerOptions = SocketIOServer.Config; + diff --git a/packages/components/service-registry/README.md b/packages/components/service-registry/README.md index 76abfa3c..95ed2ad5 100644 --- a/packages/components/service-registry/README.md +++ b/packages/components/service-registry/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -34,6 +35,7 @@ - [**Module's Programmatic Interface**](#modules-programmatic-interface) - [**Module's REST-API Interface**](#modules-rest-api-interface) - [**Module's Control Interface**](#modules-control-interface) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** @@ -199,12 +201,12 @@ const service = new ServiceRegistry( - `metadata` (`Metadata`): Metadata information of the application or microservice. This information is used to identify the application in the logs, metrics, and traces... and is shown in the service observability endpoints. - **Properties**: - - `name` (`string`): Name of the application or microservice. + - `name` (`string`): Name used to identify the application or microservice, it could be node name, or the name of the application. - `description` (`string`): Description of the application or microservice. - `version` (`string`): Version of the application or microservice. - `release` (`string`): Release of the application or microservice. - `instanceId` (`string`): Unique identifier of the application or microservice. This value is generated by the application if it is not provided. - - `serviceId` (`string`): Human readable identifier of the application or microservice, should be unique in the system. + - `serviceId` (`string`): Human readable identifier of the application or microservice. - `serviceGroupId` (`string`): Group of the application or microservice to which it belongs. - `namespace` (`string`): Service namespace, used to identify declare which namespace the service belongs to. It must start with `x-` as it is a custom namespace and will be used for custom headers, openc2 commands, etc. - `tags` (`string[]`): Tags of the application or microservice. @@ -221,6 +223,8 @@ const service = new ServiceRegistry( release: '0', description: undefined, instanceId: '12345678-1234-...', // This value is generated by the application + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', } ``` @@ -374,11 +378,9 @@ Please check the documentation of the packages [@mdf.js/openc2](https://www.npmj ## **Environment variables** -- **CONFIG\_CUSTOM\_PRESET**: Default custom config loader options -- **CONFIG\_SERVICE\_REGISTRY\_PRESET**: Default service registry config loader options -- **NODE\_APP\_INSTANCE**: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics -- **NODE\_APP\_INSTANCE**: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics -- **NODE\_APP\_INSTANCE**: Create a new instance of MetricsAggregator @param logger - Instance for logging @param port - Optional AggregatorRegistry for cluster metrics +- **CONFIG\_CUSTOM\_PRESET** (default: `undefined`): Custom config preset selector, used to load a specific preset from the custom config folder. Default files to search for are \`./config/custom/presets/\*.preset.\*\` This preset is used for the custom config. +- **CONFIG\_SERVICE\_REGISTRY\_PRESET** (default: `undefined`): Service registry preset selector, used to load a specific preset from the service registry config folder. Default files to search for are \`./config/presets/\*.preset.\*\` This preset is used for the service registry config. +- **CONFIG\_APP\_NAME** (default: `'mdf-app'`): Application name ## **License** diff --git a/packages/components/service-registry/package.json b/packages/components/service-registry/package.json index 6291b119..a683d1d1 100644 --- a/packages/components/service-registry/package.json +++ b/packages/components/service-registry/package.json @@ -39,23 +39,22 @@ "@mdf.js/service-setup-provider": "*", "@mdf.js/utils": "*", "escalade": "^3.2.0", - "express": "^4.21.1", + "express": "^4.21.2", "http-proxy-middleware": "^3.0.3", "lodash": "^4.17.21", "markdown-it": "^14.1.0", "normalize-package-data": "^7.0.0", "prom-client": "^15.1.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" + "tslib": "^2.8.1", + "uuid": "^11.0.3" }, "devDependencies": { "@mdf.js/repo-config": "*", "@types/express": "^4.17.21", - "@types/lodash": "^4.17.10", + "@types/lodash": "^4.17.13", "@types/markdown-it": "^14.1.1", "@types/normalize-package-data": "^2.4.1", "@types/supertest": "^6.0.2", - "@types/uuid": "^10.0.0", "supertest": "^7.0.0" }, "engines": { diff --git a/packages/components/service-registry/src/ServiceRegistry.test.ts b/packages/components/service-registry/src/ServiceRegistry.test.ts index e3bfbfee..7c5056d3 100644 --- a/packages/components/service-registry/src/ServiceRegistry.test.ts +++ b/packages/components/service-registry/src/ServiceRegistry.test.ts @@ -1,1280 +1,1286 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Health, Layer } from '@mdf.js/core'; -import { overallStatus } from '@mdf.js/core/dist/Health'; -import { CommandJobHandler, Control } from '@mdf.js/openc2'; -import cluster from 'cluster'; -import EventEmitter from 'events'; -import { v4 } from 'uuid'; -import { ServiceRegistry } from './ServiceRegistry'; - -class ResourceMock extends EventEmitter implements Layer.App.Resource { - componentId = 'myComponentId'; - rejectStart = false; - rejectStop = false; - rejectClose = false; - constructor(public name: string) { - super(); - } - start(): Promise { - if (this.rejectStart) { - return Promise.reject(new Error('start error')); - } - return Promise.resolve(); - } - stop(): Promise { - if (this.rejectStop) { - return Promise.reject(new Error('stop error')); - } - return Promise.resolve(); - } - close(): Promise { - if (this.rejectClose) { - return Promise.reject(new Error('close error')); - } - return Promise.resolve(); - } - get status(): Health.Status { - return overallStatus(this.checks); - } - get checks(): Health.Checks { - const check: Health.Check = { - status: 'pass', - componentId: 'myComponentId', - }; - return { - [`${this.name}:status`]: [check], - }; - } -} -class ResourceMockWithOutMethod extends EventEmitter { - componentId = 'myComponentId'; - rejectStart = false; - rejectStop = false; - rejectClose = false; - constructor(public name: string) { - super(); - } - get checks(): Health.Checks { - const check: Health.Check = { - status: 'pass', - componentId: 'myComponentId', - }; - return { - [`${this.name}:status`]: [check], - }; - } -} -describe('#ServiceRegistry class', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - describe('#Happy path', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - it('Should create a valid instance with default values', async () => { - const wrapper = new ServiceRegistry<{ test: string }>(); - expect(wrapper).toBeInstanceOf(ServiceRegistry); - wrapper.register(new ResourceMock('oneResource')); - wrapper.register([new ResourceMock('twoResource'), new ResourceMock('threeResource')]); - //@ts-ignore private property - const health = wrapper._observability.health; - const checks = health.checks as Health.Checks; - expect(health).toEqual({ - name: 'mdf-app', - description: undefined, - release: '0.0.0', - version: '0', - //@ts-ignore private property - instanceId: wrapper._settingsManager.instanceId, - notes: [], - output: undefined, - status: 'warn', - checks: { - 'mdf-app:uptime': [ - { - componentId: checks['mdf-app:uptime'][0].componentId, - componentType: 'system', - observedValue: checks['mdf-app:uptime'][0].observedValue, - observedUnit: 'time', - status: 'pass', - time: checks['mdf-app:uptime'][0].time, - processId: checks['mdf-app:uptime'][0]['processId'], - }, - ], - 'mdf-app:settings': [ - { - componentId: checks['mdf-app:settings'][0].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'ServiceRegistry', - status: 'warn', - time: checks['mdf-app:settings'][0].time, - }, - { - componentId: checks['mdf-app:settings'][1].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'CustomSettings', - status: 'warn', - time: checks['mdf-app:settings'][1].time, - }, - ], - 'oneResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - 'twoResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - 'threeResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - }, - }); - expect(wrapper.logger).toBeDefined(); - }, 300); - it('Should call `process.exit` if SIGINT or SIGTERM', done => { - const wrapper = new ServiceRegistry<{ test: string }>(); - jest.spyOn(process, 'exit').mockImplementation(() => { - return undefined as never; - }); - //@ts-ignore private property - jest.spyOn(wrapper, 'shutdown').mockResolvedValue(); - process.emit('SIGINT'); - process.emit('SIGTERM'); - setTimeout(() => { - //@ts-ignore private property - expect(wrapper.shutdown).toHaveBeenCalledTimes(2); - done(); - }, 1001); - }, 2000); - it('Should create a valid instance with default values and consumer', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { consumerOptions: {}, adapterOptions: { type: 'redis' } } - ); - expect(wrapper).toBeInstanceOf(ServiceRegistry); - wrapper.register(new ResourceMock('oneResource')); - wrapper.register([new ResourceMock('twoResource'), new ResourceMock('threeResource')]); - //@ts-ignore private property - const health = wrapper._observability.health; - const checks = health.checks as Health.Checks; - expect(health).toEqual({ - name: 'mdf-app', - description: undefined, - release: '0.0.0', - version: '0', - //@ts-ignore private property - instanceId: wrapper._settingsManager.instanceId, - notes: [], - output: undefined, - status: 'warn', - checks: { - 'mdf-app:commands': [ - { - status: 'pass', - componentId: checks['mdf-app:commands'][0].componentId, - componentType: 'source', - observedValue: 0, - observedUnit: 'pending commands', - time: checks['mdf-app:commands'][0].time, - output: undefined, - }, - ], - 'mdf-app-publisher:status': [ - { - status: 'warn', - componentId: checks['mdf-app-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: checks['mdf-app-publisher:status'][0].time, - output: undefined, - }, - ], - 'mdf-app-subscriber:status': [ - { - status: 'warn', - componentId: checks['mdf-app-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: checks['mdf-app-subscriber:status'][0].time, - output: undefined, - }, - ], - 'mdf-app:lastOperation': [ - { - status: 'pass', - componentId: checks['mdf-app:lastOperation'][0].componentId, - componentType: 'adapter', - observedValue: 'ok', - observedUnit: 'result of last operation', - time: undefined, - output: undefined, - }, - ], - 'mdf-app:uptime': [ - { - componentId: checks['mdf-app:uptime'][0].componentId, - componentType: 'system', - observedValue: checks['mdf-app:uptime'][0].observedValue, - observedUnit: 'time', - status: 'pass', - time: checks['mdf-app:uptime'][0].time, - processId: checks['mdf-app:uptime'][0]['processId'], - }, - ], - 'mdf-app:settings': [ - { - componentId: checks['mdf-app:settings'][0].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'ServiceRegistry', - status: 'warn', - time: checks['mdf-app:settings'][0].time, - }, - { - componentId: checks['mdf-app:settings'][1].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'CustomSettings', - status: 'warn', - time: checks['mdf-app:settings'][1].time, - }, - ], - 'oneResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - 'twoResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - 'threeResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - }, - }); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer.instance, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer.instance, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - await wrapper.start(); - await wrapper.stop(); - }, 300); - it('Should create a valid instance with non-default values with redis adapter', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - description: 'myDescription', - release: '2.0.1', - namespace: 'x-myNamespace', - version: '2', - links: { - self: 'http://localhost:3000', - about: 'http://localhost:3000/about', - related: 'http://localhost:3000/related', - }, - tags: ['test', 'test2'], - serviceGroupId: 'myGroupId', - serviceId: 'myServiceId', - }, - adapterOptions: { - type: 'redis', - config: { - connectionName: 'myConnectionTest', - }, - }, - consumerOptions: { - id: 'myConsumerId', - resolver: { - 'query:x-myNamespace:other': (): Promise => Promise.resolve(3), - }, - actionTargetPairs: { - query: ['x-myNamespace:other'], - }, - }, - loggerOptions: { - console: { - level: 'debug', - enabled: true, - }, - }, - observabilityOptions: { - host: '0.0.0.0', - }, - retryOptions: { - attempts: 2, - }, - } - ); - expect(wrapper).toBeInstanceOf(ServiceRegistry); - //@ts-ignore private property - const health = wrapper._observability.health; - const checks = health.checks as Health.Checks; - expect(health).toEqual({ - name: 'test', - description: 'myDescription', - release: '2.0.1', - version: '2', - //@ts-ignore private property - instanceId: wrapper._settingsManager.instanceId, - notes: [], - output: undefined, - serviceGroupId: 'myGroupId', - serviceId: 'myServiceId', - status: 'warn', - tags: ['test', 'test2'], - checks: { - 'myConsumerId:commands': [ - { - status: 'pass', - componentId: checks['myConsumerId:commands'][0].componentId, - componentType: 'source', - observedValue: 0, - observedUnit: 'pending commands', - time: checks['myConsumerId:commands'][0].time, - output: undefined, - }, - ], - 'myConsumerId-publisher:status': [ - { - status: 'warn', - componentId: checks['myConsumerId-publisher:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: checks['myConsumerId-publisher:status'][0].time, - output: undefined, - }, - ], - 'myConsumerId-subscriber:status': [ - { - status: 'warn', - componentId: checks['myConsumerId-subscriber:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: checks['myConsumerId-subscriber:status'][0].time, - output: undefined, - }, - ], - 'myConsumerId:lastOperation': [ - { - status: 'pass', - componentId: checks['myConsumerId:lastOperation'][0].componentId, - componentType: 'adapter', - observedValue: 'ok', - observedUnit: 'result of last operation', - time: undefined, - output: undefined, - }, - ], - 'test:uptime': [ - { - componentId: checks['test:uptime'][0].componentId, - componentType: 'system', - observedValue: checks['test:uptime'][0].observedValue, - observedUnit: 'time', - processId: process.pid, - status: 'pass', - time: checks['test:uptime'][0].time, - }, - ], - 'test:settings': [ - { - componentId: checks['test:settings'][0].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'ServiceRegistry', - status: 'warn', - time: checks['test:settings'][0].time, - }, - { - componentId: checks['test:settings'][1].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'CustomSettings', - status: 'warn', - time: checks['test:settings'][1].time, - }, - ], - }, - links: { - about: 'http://localhost:3000/about', - related: 'http://localhost:3000/related', - self: 'http://localhost:3000', - }, - }); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'query:x-myNamespace:errors' - ); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'query:x-myNamespace:health' - ); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'query:x-myNamespace:other' - ); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'query:x-myNamespace:stats' - ); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'start:x-myNamespace:resources' - ); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'stop:x-myNamespace:resources' - ); - }, 300); - it('Should create a valid instance with non-default values with socket-io adapter', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - description: 'myDescription', - release: '2.0.1', - version: '2', - links: { - self: 'http://localhost:3000', - about: 'http://localhost:3000/about', - related: 'http://localhost:3000/related', - }, - tags: ['test', 'test2'], - serviceGroupId: 'myGroupId', - serviceId: 'myServiceId', - }, - adapterOptions: { - type: 'socketIO', - config: { - host: 'localhost', - }, - }, - consumerOptions: { - id: 'myConsumerId', - actionTargetPairs: { - query: ['x-myNamespace:other'], - }, - resolver: { - 'query:x-myNamespace:other': (): Promise => Promise.resolve(3), - }, - }, - loggerOptions: { - console: { - level: 'debug', - enabled: true, - }, - }, - observabilityOptions: { - host: '0.0.0.0', - }, - retryOptions: { - attempts: 2, - }, - } - ); - expect(wrapper).toBeInstanceOf(ServiceRegistry); - //@ts-ignore private property - const health = wrapper._observability.health; - const checks = health.checks as Health.Checks; - expect(health).toEqual({ - name: 'test', - description: 'myDescription', - version: '2', - release: '2.0.1', - //@ts-ignore private property - instanceId: wrapper._settingsManager.instanceId, - serviceId: 'myServiceId', - serviceGroupId: 'myGroupId', - tags: ['test', 'test2'], - links: { - self: 'http://localhost:3000', - about: 'http://localhost:3000/about', - related: 'http://localhost:3000/related', - }, - notes: [], - output: undefined, - status: 'warn', - checks: { - 'test:settings': [ - { - componentId: checks['test:settings'][0].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'ServiceRegistry', - status: 'warn', - time: checks['test:settings'][0].time, - }, - { - componentId: checks['test:settings'][1].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'CustomSettings', - status: 'warn', - time: checks['test:settings'][1].time, - }, - ], - 'myConsumerId:commands': [ - { - status: 'pass', - componentId: checks['myConsumerId:commands'][0].componentId, - componentType: 'source', - observedValue: 0, - observedUnit: 'pending commands', - time: checks['myConsumerId:commands'][0].time, - output: undefined, - }, - ], - 'myConsumerId:status': [ - { - status: 'warn', - componentId: checks['myConsumerId:status'][0].componentId, - componentType: 'service', - observedValue: 'stopped', - time: checks['myConsumerId:status'][0].time, - output: undefined, - }, - ], - 'myConsumerId:lastOperation': [ - { - status: 'pass', - componentId: checks['myConsumerId:lastOperation'][0].componentId, - componentType: 'adapter', - observedValue: 'ok', - observedUnit: 'result of last operation', - time: undefined, - output: undefined, - }, - ], - 'test:uptime': [ - { - componentId: checks['test:uptime'][0].componentId, - componentType: 'system', - observedValue: checks['test:uptime'][0].observedValue, - observedUnit: 'time', - processId: process.pid, - status: 'pass', - time: checks['test:uptime'][0].time, - }, - ], - }, - }); - //@ts-ignore - private property - expect(wrapper._consumer.instance.options.resolver).toHaveProperty( - 'query:x-myNamespace:other' - ); - }, 300); - it('Should bootstrap and shutdown properly', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { consumerOptions: {}, adapterOptions: { type: 'redis' } } - ); - const resource = new ResourceMock('oneResource'); - wrapper.register(resource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - //@ts-ignore - private property - await wrapper.bootstrap(); - //@ts-ignore - private property - await wrapper.bootstrap(); - //@ts-ignore - private property - expect(wrapper._observability.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeTruthy(); - //@ts-ignore - private property - await wrapper.shutdown(); - //@ts-ignore - private property - await wrapper.shutdown(); - //@ts-ignore - private property - expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - }, 300); - it('Should start and stop properly', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { consumerOptions: {}, adapterOptions: { type: 'redis' } } - ); - const resource = new ResourceMock('oneResource'); - const otherResource = new ResourceMockWithOutMethod('otherResourceWithOutMethod'); - wrapper.register(resource); - //@ts-ignore - private property - wrapper.register(otherResource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); - jest.spyOn(resource, 'start').mockResolvedValue(); - jest.spyOn(resource, 'stop').mockResolvedValue(); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - //@ts-ignore - private property - expect(wrapper._started).toBeFalsy(); - await wrapper.start(); - await wrapper.start(); - //@ts-ignore - private property - expect(wrapper._observability.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.start).toHaveBeenCalledTimes(1); - expect(resource.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeTruthy(); - //@ts-ignore - private property - expect(wrapper._started).toBeTruthy(); - await wrapper.stop(); - await wrapper.stop(); - //@ts-ignore - private property - expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.stop).toHaveBeenCalledTimes(1); - expect(resource.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - //@ts-ignore - private property - expect(wrapper._started).toBeFalsy(); - }, 300); - it('Should execute the commands', async () => { - const myCommandResolver = jest.fn(() => Promise.resolve(3)); - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - namespace: 'x-myNamespace', - }, - consumerOptions: { - actionTargetPairs: { - query: ['x-myNamespace:other', 'x-myNamespace:another'], - }, - resolver: { - 'query:x-myNamespace:another': myCommandResolver, - }, - }, - adapterOptions: { type: 'redis' }, - } - ); - const resource = new ResourceMock('oneResource'); - wrapper.register(resource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer.instance, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer.instance, 'stop').mockResolvedValue(); - jest.spyOn(resource, 'start').mockResolvedValue(); - jest.spyOn(resource, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability._metricsRegistry, 'metricsJSON').mockResolvedValue({ - //@ts-ignore - private property - name: 'test', - }); - const queryHealth: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Query, - target: { - 'x-myNamespace:health': {}, - }, - }, - }; - const queryStats: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Query, - target: { - 'x-myNamespace:stats': {}, - }, - }, - }; - const queryErrors: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Query, - target: { - 'x-myNamespace:errors': {}, - }, - }, - }; - const queryStop: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Stop, - target: { - 'x-myNamespace:resources': {}, - }, - }, - }; - const queryStart: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Start, - target: { - 'x-myNamespace:resources': {}, - }, - }, - }; - const otherCommand: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Query, - target: { - 'x-myNamespace:other': {}, - }, - }, - }; - const anotherCommand: Control.CommandMessage = { - from: 'my', - to: ['test'], - msg_type: Control.MessageType.Command, - request_id: v4(), - content_type: 'application/json', - created: Date.now(), - content: { - action: Control.Action.Query, - target: { - 'x-myNamespace:another': {}, - }, - }, - }; - //@ts-ignore - private property - const resultOfQueryHealth = await wrapper._consumer.instance.processCommand(queryHealth); - expect(resultOfQueryHealth).toEqual({ - content_type: 'application/openc2+json;version=1.0', - msg_type: 'response', - request_id: resultOfQueryHealth.request_id, - status: 200, - created: resultOfQueryHealth.created, - from: 'test', - to: ['my'], - content: { - status: 200, - status_text: undefined, - results: { - 'x-myNamespace:health': { - name: 'test', - description: undefined, - version: '0', - release: '0.0.0', - instanceId: resultOfQueryHealth.content.results['x-myNamespace:health'].instanceId, - notes: [], - output: undefined, - status: 'warn', - checks: { - 'oneResource:status': [ - { - status: 'pass', - componentId: 'myComponentId', - }, - ], - 'test:commands': [ - { - status: 'pass', - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:commands' - ][0].componentId, - componentType: 'source', - observedValue: 0, - observedUnit: 'pending commands', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:commands' - ][0].time, - output: undefined, - }, - ], - 'test-publisher:status': [ - { - status: 'warn', - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test-publisher:status' - ][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test-publisher:status' - ][0].time, - output: undefined, - }, - ], - 'test-subscriber:status': [ - { - status: 'warn', - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test-subscriber:status' - ][0].componentId, - componentType: 'database', - observedValue: 'stopped', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test-subscriber:status' - ][0].time, - output: undefined, - }, - ], - 'test:lastOperation': [ - { - status: 'pass', - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:lastOperation' - ][0].componentId, - componentType: 'adapter', - observedValue: 'ok', - observedUnit: 'result of last operation', - time: undefined, - output: undefined, - }, - ], - 'test:uptime': [ - { - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:uptime' - ][0].componentId, - componentType: 'system', - observedValue: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:uptime' - ][0].observedValue, - observedUnit: 'time', - processId: process.pid, - status: 'pass', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:uptime' - ][0].time, - }, - ], - ['test:settings']: [ - { - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:settings' - ][0].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'ServiceRegistry', - status: 'warn', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:settings' - ][0].time, - }, - { - componentId: - resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:settings' - ][1].componentId, - componentType: 'setup service', - observedUnit: 'status', - observedValue: 'stopped', - output: undefined, - scope: 'CustomSettings', - status: 'warn', - time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ - 'test:settings' - ][1].time, - }, - ], - }, - }, - }, - }, - }); - //@ts-ignore - private property - const resultOfQueryStats = await wrapper._consumer.instance.processCommand(queryStats); - expect(resultOfQueryStats).toEqual({ - content_type: 'application/openc2+json;version=1.0', - msg_type: 'response', - request_id: resultOfQueryStats.request_id, - status: 200, - created: resultOfQueryStats.created, - from: 'test', - to: ['my'], - content: { - status: 200, - status_text: undefined, - results: { - 'x-myNamespace:stats': { - name: 'test', - }, - }, - }, - }); - //@ts-ignore - private property - const resultOfQueryErrors = await wrapper._consumer.instance.processCommand(queryErrors); - expect(resultOfQueryErrors).toEqual({ - content_type: 'application/openc2+json;version=1.0', - msg_type: 'response', - request_id: resultOfQueryErrors.request_id, - status: 200, - created: resultOfQueryErrors.created, - from: 'test', - to: ['my'], - content: { - status: 200, - status_text: undefined, - results: { - 'x-myNamespace:errors': [], - }, - }, - }); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - //@ts-ignore - private property - expect(wrapper._started).toBeFalsy(); - //@ts-ignore - private property - await wrapper._consumer.instance.processCommand(queryStart); - //@ts-ignore - private property - await wrapper._consumer.instance.processCommand(queryStart); - //@ts-ignore - private property - expect(wrapper._observability.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.instance.start).toHaveBeenCalledTimes(1); - expect(resource.start).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeTruthy(); - //@ts-ignore - private property - expect(wrapper._started).toBeTruthy(); - //@ts-ignore - private property - wrapper._consumer.instance.emit('error', new Error('Error starting')); - expect(wrapper.errors[0].message).toEqual('Error starting'); - wrapper.on('command', (command: CommandJobHandler) => { - command.data; - command.done(); - }); - //@ts-ignore - private property - const resultOfOther = await wrapper._consumer.instance.processCommand(otherCommand); - expect(resultOfOther).toEqual({ - content_type: 'application/openc2+json;version=1.0', - msg_type: 'response', - request_id: resultOfOther.request_id, - status: 200, - created: resultOfOther.created, - from: 'test', - to: ['my'], - content: { - status: 200, - status_text: undefined, - results: undefined, - }, - }); - //@ts-ignore - private property - const resultOfAnother = await wrapper._consumer.instance.processCommand(anotherCommand); - expect(resultOfAnother).toEqual({ - content_type: 'application/openc2+json;version=1.0', - msg_type: 'response', - request_id: resultOfAnother.request_id, - status: 200, - created: resultOfAnother.created, - from: 'test', - to: ['my'], - content: { - status: 200, - status_text: undefined, - results: { 'x-myNamespace:another': 3 }, - }, - }); - //@ts-ignore - private property - await wrapper._consumer.instance.processCommand(queryStop); - //@ts-ignore - private property - await wrapper._consumer.instance.processCommand(queryStop); - //@ts-ignore - private property - expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._consumer.instance.stop).toHaveBeenCalledTimes(1); - expect(resource.stop).toHaveBeenCalledTimes(1); - //@ts-ignore - private property - expect(wrapper._booted).toBeFalsy(); - //@ts-ignore - private property - expect(wrapper._started).toBeFalsy(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - }, 300); - it('Should create a valid instance as a Primary node in a cluster', async () => { - jest.replaceProperty(cluster, 'isPrimary', true); - const wrapper = new ServiceRegistry({}, { observabilityOptions: { isCluster: true } }); - const resource = new ResourceMock('oneResource'); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - jest.spyOn(resource, 'start').mockResolvedValue(); - jest.spyOn(resource, 'stop').mockResolvedValue(); - wrapper.register(resource); - await wrapper.start(); - await wrapper.stop(); - expect(resource.start).toHaveBeenCalledTimes(0); - expect(resource.stop).toHaveBeenCalledTimes(0); - }, 300); - it('Should create a valid instance as a Worker node in a cluster', async () => { - jest.replaceProperty(cluster, 'isPrimary', false); - const wrapper = new ServiceRegistry({}, { observabilityOptions: { isCluster: true } }); - const resource = new ResourceMock('oneResource'); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - jest.spyOn(resource, 'start').mockResolvedValue(); - jest.spyOn(resource, 'stop').mockResolvedValue(); - wrapper.register(resource); - await wrapper.start(); - await wrapper.stop(); - expect(resource.start).toHaveBeenCalledTimes(1); - expect(resource.stop).toHaveBeenCalledTimes(1); - jest.restoreAllMocks(); - }, 300); - }); - describe('#Sad path', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - it('Should add an error in the list if the adapter is not valid', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - }, - retryOptions: { - attempts: 1, - }, - consumerOptions: {}, - adapterOptions: { - //@ts-ignore - Invalid adapter - type: 'invalid', - }, - } - ); - expect(wrapper.status).toEqual('warn'); - expect(wrapper.errors.length).toEqual(1); - expect(wrapper.errors[0].message).toEqual( - 'Error in the OpenC2 Consumer instance configuration' - ); - const checks = wrapper.health.checks as Health.Checks; - expect(checks['test:lastOperation']).toBeUndefined(); - }, 300); - it('Should throw an error if try to bootstrap and its not possible', async () => { - try { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - }, - retryOptions: { - attempts: 1, - maxWaitTime: 100, - timeout: 120, - waitTime: 100, - }, - consumerOptions: {}, - adapterOptions: { type: 'redis' }, - } - ); - //@ts-ignore - private property - await wrapper.bootstrap(); - throw new Error('Should not be here'); - } catch (error) { - expect((error as Error).message).toEqual( - 'Error bootstrapping the application engine: Too much attempts [1], the promise will not be retried' - ); - } - }, 300); - it('Should throw an error in try to shutdown and its not possible', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - }, - retryOptions: { - attempts: 1, - }, - consumerOptions: {}, - } - ); - const resource = new ResourceMock('oneResource'); - wrapper.register(resource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockRejectedValue(new Error('Error stopping')); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); - //@ts-ignore - private property - await wrapper.bootstrap(); - try { - //@ts-ignore - private property - await wrapper.shutdown(); - throw new Error('Should not be here'); - } catch (error) { - expect((error as Error).message).toEqual( - 'Error shutting down the application engine: Too much attempts [1], the promise will not be retried' - ); - } - }, 300); - it('Should throw an error in try to start and its not possible', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - }, - retryOptions: { - attempts: 1, - }, - consumerOptions: {}, - } - ); - const resource = new ResourceMock('oneResource'); - resource.rejectStart = true; - wrapper.register(resource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); - try { - await wrapper.start(); - throw new Error('Should not be here'); - } catch (error) { - expect((error as Error).message).toEqual( - 'Error starting the application resources: Too much attempts [1], the promise will not be retried' - ); - } - }, 300); - it('Should throw an error in try to stop and its not possible', async () => { - const wrapper = new ServiceRegistry( - { consumer: true }, - { - metadata: { - name: 'test', - }, - retryOptions: { - attempts: 1, - }, - consumerOptions: {}, - } - ); - const resource = new ResourceMock('oneResource'); - resource.rejectStop = true; - wrapper.register(resource); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); - //@ts-ignore - private property - jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); - try { - await wrapper.start(); - await wrapper.stop(); - throw new Error('Should not be here'); - } catch (error) { - expect((error as Error).message).toEqual( - 'Error stopping the application resources: Too much attempts [1], the promise will not be retried' - ); - } - }, 300); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Health, Layer } from '@mdf.js/core'; +import { overallStatus } from '@mdf.js/core/dist/Health'; +import { CommandJobHandler, Control } from '@mdf.js/openc2'; +import cluster from 'cluster'; +import EventEmitter from 'events'; +import { v4 } from 'uuid'; +import { ServiceRegistry } from './ServiceRegistry'; + +class ResourceMock extends EventEmitter implements Layer.App.Resource { + componentId = 'myComponentId'; + rejectStart = false; + rejectStop = false; + rejectClose = false; + constructor(public name: string) { + super(); + } + start(): Promise { + if (this.rejectStart) { + return Promise.reject(new Error('start error')); + } + return Promise.resolve(); + } + stop(): Promise { + if (this.rejectStop) { + return Promise.reject(new Error('stop error')); + } + return Promise.resolve(); + } + close(): Promise { + if (this.rejectClose) { + return Promise.reject(new Error('close error')); + } + return Promise.resolve(); + } + get status(): Health.Status { + return overallStatus(this.checks); + } + get checks(): Health.Checks { + const check: Health.Check = { + status: 'pass', + componentId: 'myComponentId', + }; + return { + [`${this.name}:status`]: [check], + }; + } +} +class ResourceMockWithOutMethod extends EventEmitter { + componentId = 'myComponentId'; + rejectStart = false; + rejectStop = false; + rejectClose = false; + constructor(public name: string) { + super(); + } + get checks(): Health.Checks { + const check: Health.Check = { + status: 'pass', + componentId: 'myComponentId', + }; + return { + [`${this.name}:status`]: [check], + }; + } +} +describe('#ServiceRegistry class', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + describe('#Happy path', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + it('Should create a valid instance with default values', async () => { + const wrapper = new ServiceRegistry<{ test: string }>(); + expect(wrapper).toBeInstanceOf(ServiceRegistry); + wrapper.register(new ResourceMock('oneResource')); + wrapper.register([new ResourceMock('twoResource'), new ResourceMock('threeResource')]); + //@ts-ignore private property + const health = wrapper._observability.health; + const checks = health.checks as Health.Checks; + expect(health).toEqual({ + name: 'mdf-app', + description: undefined, + release: '0.0.0', + version: '0', + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + //@ts-ignore private property + instanceId: wrapper._settingsManager.instanceId, + notes: [], + output: undefined, + status: 'warn', + checks: { + 'mdf-app:uptime': [ + { + componentId: checks['mdf-app:uptime'][0].componentId, + componentType: 'system', + observedValue: checks['mdf-app:uptime'][0].observedValue, + observedUnit: 'time', + status: 'pass', + time: checks['mdf-app:uptime'][0].time, + processId: checks['mdf-app:uptime'][0]['processId'], + }, + ], + 'mdf-app:settings': [ + { + componentId: checks['mdf-app:settings'][0].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'ServiceRegistry', + status: 'warn', + time: checks['mdf-app:settings'][0].time, + }, + { + componentId: checks['mdf-app:settings'][1].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'CustomSettings', + status: 'warn', + time: checks['mdf-app:settings'][1].time, + }, + ], + 'oneResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + 'twoResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + 'threeResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + }, + }); + expect(wrapper.logger).toBeDefined(); + }, 300); + it('Should call `process.exit` if SIGINT or SIGTERM', done => { + const wrapper = new ServiceRegistry<{ test: string }>(); + jest.spyOn(process, 'exit').mockImplementation(() => { + return undefined as never; + }); + //@ts-ignore private property + jest.spyOn(wrapper, 'shutdown').mockResolvedValue(); + process.emit('SIGINT'); + process.emit('SIGTERM'); + setTimeout(() => { + //@ts-ignore private property + expect(wrapper.shutdown).toHaveBeenCalledTimes(2); + done(); + }, 1001); + }, 2000); + it('Should create a valid instance with default values and consumer', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { consumerOptions: {}, adapterOptions: { type: 'redis' } } + ); + expect(wrapper).toBeInstanceOf(ServiceRegistry); + wrapper.register(new ResourceMock('oneResource')); + wrapper.register([new ResourceMock('twoResource'), new ResourceMock('threeResource')]); + //@ts-ignore private property + const health = wrapper._observability.health; + const checks = health.checks as Health.Checks; + expect(health).toEqual({ + name: 'mdf-app', + description: undefined, + release: '0.0.0', + version: '0', + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + //@ts-ignore private property + instanceId: wrapper._settingsManager.instanceId, + notes: [], + output: undefined, + status: 'warn', + checks: { + 'mdf-app:commands': [ + { + status: 'pass', + componentId: checks['mdf-app:commands'][0].componentId, + componentType: 'source', + observedValue: 0, + observedUnit: 'pending commands', + time: checks['mdf-app:commands'][0].time, + output: undefined, + }, + ], + 'mdf-app-publisher:status': [ + { + status: 'warn', + componentId: checks['mdf-app-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: checks['mdf-app-publisher:status'][0].time, + output: undefined, + }, + ], + 'mdf-app-subscriber:status': [ + { + status: 'warn', + componentId: checks['mdf-app-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: checks['mdf-app-subscriber:status'][0].time, + output: undefined, + }, + ], + 'mdf-app:lastOperation': [ + { + status: 'pass', + componentId: checks['mdf-app:lastOperation'][0].componentId, + componentType: 'adapter', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: undefined, + output: undefined, + }, + ], + 'mdf-app:uptime': [ + { + componentId: checks['mdf-app:uptime'][0].componentId, + componentType: 'system', + observedValue: checks['mdf-app:uptime'][0].observedValue, + observedUnit: 'time', + status: 'pass', + time: checks['mdf-app:uptime'][0].time, + processId: checks['mdf-app:uptime'][0]['processId'], + }, + ], + 'mdf-app:settings': [ + { + componentId: checks['mdf-app:settings'][0].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'ServiceRegistry', + status: 'warn', + time: checks['mdf-app:settings'][0].time, + }, + { + componentId: checks['mdf-app:settings'][1].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'CustomSettings', + status: 'warn', + time: checks['mdf-app:settings'][1].time, + }, + ], + 'oneResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + 'twoResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + 'threeResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + }, + }); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer.instance, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer.instance, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + await wrapper.start(); + await wrapper.stop(); + }, 300); + it('Should create a valid instance with non-default values with redis adapter', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + description: 'myDescription', + release: '2.0.1', + namespace: 'x-myNamespace', + version: '2', + links: { + self: 'http://localhost:3000', + about: 'http://localhost:3000/about', + related: 'http://localhost:3000/related', + }, + tags: ['test', 'test2'], + serviceGroupId: 'myGroupId', + serviceId: 'myServiceId', + }, + adapterOptions: { + type: 'redis', + config: { + connectionName: 'myConnectionTest', + }, + }, + consumerOptions: { + id: 'myConsumerId', + resolver: { + 'query:x-myNamespace:other': (): Promise => Promise.resolve(3), + }, + actionTargetPairs: { + query: ['x-myNamespace:other'], + }, + }, + loggerOptions: { + console: { + level: 'debug', + enabled: true, + }, + }, + observabilityOptions: { + host: '0.0.0.0', + }, + retryOptions: { + attempts: 2, + }, + } + ); + expect(wrapper).toBeInstanceOf(ServiceRegistry); + //@ts-ignore private property + const health = wrapper._observability.health; + const checks = health.checks as Health.Checks; + expect(health).toEqual({ + name: 'test', + description: 'myDescription', + release: '2.0.1', + version: '2', + //@ts-ignore private property + instanceId: wrapper._settingsManager.instanceId, + notes: [], + output: undefined, + serviceGroupId: 'myGroupId', + serviceId: 'myServiceId', + status: 'warn', + tags: ['test', 'test2'], + checks: { + 'myConsumerId:commands': [ + { + status: 'pass', + componentId: checks['myConsumerId:commands'][0].componentId, + componentType: 'source', + observedValue: 0, + observedUnit: 'pending commands', + time: checks['myConsumerId:commands'][0].time, + output: undefined, + }, + ], + 'myConsumerId-publisher:status': [ + { + status: 'warn', + componentId: checks['myConsumerId-publisher:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: checks['myConsumerId-publisher:status'][0].time, + output: undefined, + }, + ], + 'myConsumerId-subscriber:status': [ + { + status: 'warn', + componentId: checks['myConsumerId-subscriber:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: checks['myConsumerId-subscriber:status'][0].time, + output: undefined, + }, + ], + 'myConsumerId:lastOperation': [ + { + status: 'pass', + componentId: checks['myConsumerId:lastOperation'][0].componentId, + componentType: 'adapter', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: undefined, + output: undefined, + }, + ], + 'test:uptime': [ + { + componentId: checks['test:uptime'][0].componentId, + componentType: 'system', + observedValue: checks['test:uptime'][0].observedValue, + observedUnit: 'time', + processId: process.pid, + status: 'pass', + time: checks['test:uptime'][0].time, + }, + ], + 'test:settings': [ + { + componentId: checks['test:settings'][0].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'ServiceRegistry', + status: 'warn', + time: checks['test:settings'][0].time, + }, + { + componentId: checks['test:settings'][1].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'CustomSettings', + status: 'warn', + time: checks['test:settings'][1].time, + }, + ], + }, + links: { + about: 'http://localhost:3000/about', + related: 'http://localhost:3000/related', + self: 'http://localhost:3000', + }, + }); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'query:x-myNamespace:errors' + ); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'query:x-myNamespace:health' + ); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'query:x-myNamespace:other' + ); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'query:x-myNamespace:stats' + ); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'start:x-myNamespace:resources' + ); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'stop:x-myNamespace:resources' + ); + }, 300); + it('Should create a valid instance with non-default values with socket-io adapter', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + description: 'myDescription', + release: '2.0.1', + version: '2', + links: { + self: 'http://localhost:3000', + about: 'http://localhost:3000/about', + related: 'http://localhost:3000/related', + }, + tags: ['test', 'test2'], + serviceGroupId: 'myGroupId', + serviceId: 'myServiceId', + }, + adapterOptions: { + type: 'socketIO', + config: { + host: 'localhost', + }, + }, + consumerOptions: { + id: 'myConsumerId', + actionTargetPairs: { + query: ['x-myNamespace:other'], + }, + resolver: { + 'query:x-myNamespace:other': (): Promise => Promise.resolve(3), + }, + }, + loggerOptions: { + console: { + level: 'debug', + enabled: true, + }, + }, + observabilityOptions: { + host: '0.0.0.0', + }, + retryOptions: { + attempts: 2, + }, + } + ); + expect(wrapper).toBeInstanceOf(ServiceRegistry); + //@ts-ignore private property + const health = wrapper._observability.health; + const checks = health.checks as Health.Checks; + expect(health).toEqual({ + name: 'test', + description: 'myDescription', + version: '2', + release: '2.0.1', + //@ts-ignore private property + instanceId: wrapper._settingsManager.instanceId, + serviceId: 'myServiceId', + serviceGroupId: 'myGroupId', + tags: ['test', 'test2'], + links: { + self: 'http://localhost:3000', + about: 'http://localhost:3000/about', + related: 'http://localhost:3000/related', + }, + notes: [], + output: undefined, + status: 'warn', + checks: { + 'test:settings': [ + { + componentId: checks['test:settings'][0].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'ServiceRegistry', + status: 'warn', + time: checks['test:settings'][0].time, + }, + { + componentId: checks['test:settings'][1].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'CustomSettings', + status: 'warn', + time: checks['test:settings'][1].time, + }, + ], + 'myConsumerId:commands': [ + { + status: 'pass', + componentId: checks['myConsumerId:commands'][0].componentId, + componentType: 'source', + observedValue: 0, + observedUnit: 'pending commands', + time: checks['myConsumerId:commands'][0].time, + output: undefined, + }, + ], + 'myConsumerId:status': [ + { + status: 'warn', + componentId: checks['myConsumerId:status'][0].componentId, + componentType: 'service', + observedValue: 'stopped', + time: checks['myConsumerId:status'][0].time, + output: undefined, + }, + ], + 'myConsumerId:lastOperation': [ + { + status: 'pass', + componentId: checks['myConsumerId:lastOperation'][0].componentId, + componentType: 'adapter', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: undefined, + output: undefined, + }, + ], + 'test:uptime': [ + { + componentId: checks['test:uptime'][0].componentId, + componentType: 'system', + observedValue: checks['test:uptime'][0].observedValue, + observedUnit: 'time', + processId: process.pid, + status: 'pass', + time: checks['test:uptime'][0].time, + }, + ], + }, + }); + //@ts-ignore - private property + expect(wrapper._consumer.instance.options.resolver).toHaveProperty( + 'query:x-myNamespace:other' + ); + }, 300); + it('Should bootstrap and shutdown properly', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { consumerOptions: {}, adapterOptions: { type: 'redis' } } + ); + const resource = new ResourceMock('oneResource'); + wrapper.register(resource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + //@ts-ignore - private property + await wrapper.bootstrap(); + //@ts-ignore - private property + await wrapper.bootstrap(); + //@ts-ignore - private property + expect(wrapper._observability.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeTruthy(); + //@ts-ignore - private property + await wrapper.shutdown(); + //@ts-ignore - private property + await wrapper.shutdown(); + //@ts-ignore - private property + expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + }, 300); + it('Should start and stop properly', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { consumerOptions: {}, adapterOptions: { type: 'redis' } } + ); + const resource = new ResourceMock('oneResource'); + const otherResource = new ResourceMockWithOutMethod('otherResourceWithOutMethod'); + wrapper.register(resource); + //@ts-ignore - private property + wrapper.register(otherResource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); + jest.spyOn(resource, 'start').mockResolvedValue(); + jest.spyOn(resource, 'stop').mockResolvedValue(); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + //@ts-ignore - private property + expect(wrapper._started).toBeFalsy(); + await wrapper.start(); + await wrapper.start(); + //@ts-ignore - private property + expect(wrapper._observability.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.start).toHaveBeenCalledTimes(1); + expect(resource.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeTruthy(); + //@ts-ignore - private property + expect(wrapper._started).toBeTruthy(); + await wrapper.stop(); + await wrapper.stop(); + //@ts-ignore - private property + expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.stop).toHaveBeenCalledTimes(1); + expect(resource.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + //@ts-ignore - private property + expect(wrapper._started).toBeFalsy(); + }, 300); + it('Should execute the commands', async () => { + const myCommandResolver = jest.fn(() => Promise.resolve(3)); + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + namespace: 'x-myNamespace', + }, + consumerOptions: { + actionTargetPairs: { + query: ['x-myNamespace:other', 'x-myNamespace:another'], + }, + resolver: { + 'query:x-myNamespace:another': myCommandResolver, + }, + }, + adapterOptions: { type: 'redis' }, + } + ); + const resource = new ResourceMock('oneResource'); + wrapper.register(resource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer.instance, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer.instance, 'stop').mockResolvedValue(); + jest.spyOn(resource, 'start').mockResolvedValue(); + jest.spyOn(resource, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability._metricsRegistry, 'metricsJSON').mockResolvedValue({ + //@ts-ignore - private property + name: 'test', + }); + const queryHealth: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Query, + target: { + 'x-myNamespace:health': {}, + }, + }, + }; + const queryStats: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Query, + target: { + 'x-myNamespace:stats': {}, + }, + }, + }; + const queryErrors: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Query, + target: { + 'x-myNamespace:errors': {}, + }, + }, + }; + const queryStop: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Stop, + target: { + 'x-myNamespace:resources': {}, + }, + }, + }; + const queryStart: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Start, + target: { + 'x-myNamespace:resources': {}, + }, + }, + }; + const otherCommand: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Query, + target: { + 'x-myNamespace:other': {}, + }, + }, + }; + const anotherCommand: Control.CommandMessage = { + from: 'my', + to: ['test'], + msg_type: Control.MessageType.Command, + request_id: v4(), + content_type: 'application/json', + created: Date.now(), + content: { + action: Control.Action.Query, + target: { + 'x-myNamespace:another': {}, + }, + }, + }; + //@ts-ignore - private property + const resultOfQueryHealth = await wrapper._consumer.instance.processCommand(queryHealth); + expect(resultOfQueryHealth).toEqual({ + content_type: 'application/openc2+json;version=1.0', + msg_type: 'response', + request_id: resultOfQueryHealth.request_id, + status: 200, + created: resultOfQueryHealth.created, + from: 'test', + to: ['my'], + content: { + status: 200, + status_text: undefined, + results: { + 'x-myNamespace:health': { + name: 'test', + description: undefined, + version: '0', + release: '0.0.0', + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + instanceId: resultOfQueryHealth.content.results['x-myNamespace:health'].instanceId, + notes: [], + output: undefined, + status: 'warn', + checks: { + 'oneResource:status': [ + { + status: 'pass', + componentId: 'myComponentId', + }, + ], + 'test:commands': [ + { + status: 'pass', + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:commands' + ][0].componentId, + componentType: 'source', + observedValue: 0, + observedUnit: 'pending commands', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:commands' + ][0].time, + output: undefined, + }, + ], + 'test-publisher:status': [ + { + status: 'warn', + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test-publisher:status' + ][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test-publisher:status' + ][0].time, + output: undefined, + }, + ], + 'test-subscriber:status': [ + { + status: 'warn', + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test-subscriber:status' + ][0].componentId, + componentType: 'database', + observedValue: 'stopped', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test-subscriber:status' + ][0].time, + output: undefined, + }, + ], + 'test:lastOperation': [ + { + status: 'pass', + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:lastOperation' + ][0].componentId, + componentType: 'adapter', + observedValue: 'ok', + observedUnit: 'result of last operation', + time: undefined, + output: undefined, + }, + ], + 'test:uptime': [ + { + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:uptime' + ][0].componentId, + componentType: 'system', + observedValue: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:uptime' + ][0].observedValue, + observedUnit: 'time', + processId: process.pid, + status: 'pass', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:uptime' + ][0].time, + }, + ], + ['test:settings']: [ + { + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:settings' + ][0].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'ServiceRegistry', + status: 'warn', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:settings' + ][0].time, + }, + { + componentId: + resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:settings' + ][1].componentId, + componentType: 'setup service', + observedUnit: 'status', + observedValue: 'stopped', + output: undefined, + scope: 'CustomSettings', + status: 'warn', + time: resultOfQueryHealth.content.results['x-myNamespace:health'].checks[ + 'test:settings' + ][1].time, + }, + ], + }, + }, + }, + }, + }); + //@ts-ignore - private property + const resultOfQueryStats = await wrapper._consumer.instance.processCommand(queryStats); + expect(resultOfQueryStats).toEqual({ + content_type: 'application/openc2+json;version=1.0', + msg_type: 'response', + request_id: resultOfQueryStats.request_id, + status: 200, + created: resultOfQueryStats.created, + from: 'test', + to: ['my'], + content: { + status: 200, + status_text: undefined, + results: { + 'x-myNamespace:stats': { + name: 'test', + }, + }, + }, + }); + //@ts-ignore - private property + const resultOfQueryErrors = await wrapper._consumer.instance.processCommand(queryErrors); + expect(resultOfQueryErrors).toEqual({ + content_type: 'application/openc2+json;version=1.0', + msg_type: 'response', + request_id: resultOfQueryErrors.request_id, + status: 200, + created: resultOfQueryErrors.created, + from: 'test', + to: ['my'], + content: { + status: 200, + status_text: undefined, + results: { + 'x-myNamespace:errors': [], + }, + }, + }); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + //@ts-ignore - private property + expect(wrapper._started).toBeFalsy(); + //@ts-ignore - private property + await wrapper._consumer.instance.processCommand(queryStart); + //@ts-ignore - private property + await wrapper._consumer.instance.processCommand(queryStart); + //@ts-ignore - private property + expect(wrapper._observability.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.instance.start).toHaveBeenCalledTimes(1); + expect(resource.start).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeTruthy(); + //@ts-ignore - private property + expect(wrapper._started).toBeTruthy(); + //@ts-ignore - private property + wrapper._consumer.instance.emit('error', new Error('Error starting')); + expect(wrapper.errors[0].message).toEqual('Error starting'); + wrapper.on('command', (command: CommandJobHandler) => { + command.data; + command.done(); + }); + //@ts-ignore - private property + const resultOfOther = await wrapper._consumer.instance.processCommand(otherCommand); + expect(resultOfOther).toEqual({ + content_type: 'application/openc2+json;version=1.0', + msg_type: 'response', + request_id: resultOfOther.request_id, + status: 200, + created: resultOfOther.created, + from: 'test', + to: ['my'], + content: { + status: 200, + status_text: undefined, + results: undefined, + }, + }); + //@ts-ignore - private property + const resultOfAnother = await wrapper._consumer.instance.processCommand(anotherCommand); + expect(resultOfAnother).toEqual({ + content_type: 'application/openc2+json;version=1.0', + msg_type: 'response', + request_id: resultOfAnother.request_id, + status: 200, + created: resultOfAnother.created, + from: 'test', + to: ['my'], + content: { + status: 200, + status_text: undefined, + results: { 'x-myNamespace:another': 3 }, + }, + }); + //@ts-ignore - private property + await wrapper._consumer.instance.processCommand(queryStop); + //@ts-ignore - private property + await wrapper._consumer.instance.processCommand(queryStop); + //@ts-ignore - private property + expect(wrapper._observability.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._consumer.instance.stop).toHaveBeenCalledTimes(1); + expect(resource.stop).toHaveBeenCalledTimes(1); + //@ts-ignore - private property + expect(wrapper._booted).toBeFalsy(); + //@ts-ignore - private property + expect(wrapper._started).toBeFalsy(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }, 300); + it('Should create a valid instance as a Primary node in a cluster', async () => { + jest.replaceProperty(cluster, 'isPrimary', true); + const wrapper = new ServiceRegistry({}, { observabilityOptions: { isCluster: true } }); + const resource = new ResourceMock('oneResource'); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + jest.spyOn(resource, 'start').mockResolvedValue(); + jest.spyOn(resource, 'stop').mockResolvedValue(); + wrapper.register(resource); + await wrapper.start(); + await wrapper.stop(); + expect(resource.start).toHaveBeenCalledTimes(0); + expect(resource.stop).toHaveBeenCalledTimes(0); + }, 300); + it('Should create a valid instance as a Worker node in a cluster', async () => { + jest.replaceProperty(cluster, 'isPrimary', false); + const wrapper = new ServiceRegistry({}, { observabilityOptions: { isCluster: true } }); + const resource = new ResourceMock('oneResource'); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + jest.spyOn(resource, 'start').mockResolvedValue(); + jest.spyOn(resource, 'stop').mockResolvedValue(); + wrapper.register(resource); + await wrapper.start(); + await wrapper.stop(); + expect(resource.start).toHaveBeenCalledTimes(1); + expect(resource.stop).toHaveBeenCalledTimes(1); + jest.restoreAllMocks(); + }, 300); + }); + describe('#Sad path', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + it('Should add an error in the list if the adapter is not valid', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + }, + retryOptions: { + attempts: 1, + }, + consumerOptions: {}, + adapterOptions: { + //@ts-ignore - Invalid adapter + type: 'invalid', + }, + } + ); + expect(wrapper.status).toEqual('warn'); + expect(wrapper.errors.length).toEqual(1); + expect(wrapper.errors[0].message).toEqual( + 'Error in the OpenC2 Consumer instance configuration' + ); + const checks = wrapper.health.checks as Health.Checks; + expect(checks['test:lastOperation']).toBeUndefined(); + }, 300); + it('Should throw an error if try to bootstrap and its not possible', async () => { + try { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + }, + retryOptions: { + attempts: 1, + maxWaitTime: 100, + timeout: 120, + waitTime: 100, + }, + consumerOptions: {}, + adapterOptions: { type: 'redis' }, + } + ); + //@ts-ignore - private property + await wrapper.bootstrap(); + throw new Error('Should not be here'); + } catch (error) { + expect((error as Error).message).toEqual( + 'Error bootstrapping the application engine: Too much attempts [1], the promise will not be retried' + ); + } + }, 300); + it('Should throw an error in try to shutdown and its not possible', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + }, + retryOptions: { + attempts: 1, + }, + consumerOptions: {}, + } + ); + const resource = new ResourceMock('oneResource'); + wrapper.register(resource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockRejectedValue(new Error('Error stopping')); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); + //@ts-ignore - private property + await wrapper.bootstrap(); + try { + //@ts-ignore - private property + await wrapper.shutdown(); + throw new Error('Should not be here'); + } catch (error) { + expect((error as Error).message).toEqual( + 'Error shutting down the application engine: Too much attempts [1], the promise will not be retried' + ); + } + }, 300); + it('Should throw an error in try to start and its not possible', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + }, + retryOptions: { + attempts: 1, + }, + consumerOptions: {}, + } + ); + const resource = new ResourceMock('oneResource'); + resource.rejectStart = true; + wrapper.register(resource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); + try { + await wrapper.start(); + throw new Error('Should not be here'); + } catch (error) { + expect((error as Error).message).toEqual( + 'Error starting the application resources: Too much attempts [1], the promise will not be retried' + ); + } + }, 300); + it('Should throw an error in try to stop and its not possible', async () => { + const wrapper = new ServiceRegistry( + { consumer: true }, + { + metadata: { + name: 'test', + }, + retryOptions: { + attempts: 1, + }, + consumerOptions: {}, + } + ); + const resource = new ResourceMock('oneResource'); + resource.rejectStop = true; + wrapper.register(resource); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._observability, 'stop').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'start').mockResolvedValue(); + //@ts-ignore - private property + jest.spyOn(wrapper._consumer, 'stop').mockResolvedValue(); + try { + await wrapper.start(); + await wrapper.stop(); + throw new Error('Should not be here'); + } catch (error) { + expect((error as Error).message).toEqual( + 'Error stopping the application resources: Too much attempts [1], the promise will not be retried' + ); + } + }, 300); + }); +}); diff --git a/packages/components/service-registry/src/ServiceRegistry.ts b/packages/components/service-registry/src/ServiceRegistry.ts index e7f26e30..8aeaf255 100644 --- a/packages/components/service-registry/src/ServiceRegistry.ts +++ b/packages/components/service-registry/src/ServiceRegistry.ts @@ -1,433 +1,435 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { Logger, LoggerInstance, SetContext } from '@mdf.js/logger'; -import { CommandJobHandler, ResolverMap } from '@mdf.js/openc2'; -import { RetryOptions, deCycle, retryBind } from '@mdf.js/utils'; -import EventEmitter from 'events'; -import { ControlManager } from './control'; -import { ErrorRecord, Metrics, Observability } from './observability'; -import { SettingsManager } from './settings'; -import { - BootstrapOptions, - CustomSetting, - SHUTDOWN_DELAY, - ServiceRegistryOptions, - ServiceRegistrySettings, - ServiceSetting, -} from './types'; - -export declare interface ServiceRegistry { - /** - * Add a listener for the `command` event, emitted when a new command is received - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - on(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Add a listener for the `command` event, emitted when a new command is received - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - addListener(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Add a listener for the `command` event, emitted when a new command is received. This is a - * one-time event, the listener will be removed after the first emission. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - once(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes the specified listener from the listener array for the `command` event. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - off(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes the specified listener from the listener array for the `command` event. - * @param event - `command` event - * @param listener - Command event listener - * @event - */ - removeListener(event: 'command', listener: (job: CommandJobHandler) => void): this; - /** - * Removes all listeners, or those of the specified event. - * @param event - `command` event - */ - removeAllListeners(event?: 'command'): this; -} - -export class ServiceRegistry< - CustomSettings extends Record = Record, -> extends EventEmitter { - /** Service Settings manager */ - private readonly _settingsManager: SettingsManager; - /** Resources attached to the service registry observability */ - private readonly _resources: Layer.Observable[] = []; - /** Service Registry observability instance */ - private readonly _observability: Observability; - /** Service Registry control manager */ - private readonly _consumer: ControlManager; - /** Flag to indicate if the service has performed the bootstrap */ - private _booted = false; - /** Flag to indicate if the service has started */ - private _started = false; - /** Logger instance */ - private readonly _logger: LoggerInstance; - /** - * Create a new instance of the Service Registry - * @param bootstrapOptions - Bootstrap settings, define how the Custom and the Service Registry - * settings should be loaded. - * @param serviceRegistryOptions - Service Registry settings, used as a base for the Service - * Registry configuration manager. - * @param customSettings - Custom settings provided by the user, used as a base for the Custom - * configuration manager. - */ - constructor( - bootstrapOptions?: BootstrapOptions, - serviceRegistryOptions?: ServiceRegistryOptions, - customSettings?: Partial - ) { - super(); - this._settingsManager = new SettingsManager( - bootstrapOptions, - serviceRegistryOptions, - customSettings - ); - this._logger = SetContext( - new Logger(this._settingsManager.name, this._settingsManager.logger), - this._settingsManager.name, - this._settingsManager.instanceId - ); - this._observability = new Observability({ - ...this._settingsManager.observability, - logger: this._logger, - }); - this._consumer = new ControlManager( - this._settingsManager.serviceRegistrySettings, - this._logger, - this.resolverMap - ); - this._observability.attach(this._settingsManager); - if (this._consumer.instance && !this._consumer.error && bootstrapOptions?.consumer) { - this._consumer.on('command', this.onCommandEvent.bind(this)); - this._observability.attach(this._consumer.instance); - } else if (this._consumer.error) { - // Stryker disable next-line all - this._logger.warn( - 'OpenC2 Consumer is not available, the service is not able to receive commands' - ); - this._observability.push(this._consumer.error); - } - process.on('SIGINT', () => this.onFinishCommand('SIGINT')); - process.on('SIGTERM', () => this.onFinishCommand('SIGTERM')); - } - /** @returns Default resolver map for the OpenC2 Consumer interface */ - private get resolverMap(): ResolverMap | undefined { - if (this._settingsManager.namespace) { - return { - [`query:${this._settingsManager.namespace}:health`]: this.onHealthCommand.bind(this), - [`query:${this._settingsManager.namespace}:stats`]: this.onStatsCommand.bind(this), - [`query:${this._settingsManager.namespace}:errors`]: this.onErrorsCommand.bind(this), - [`query:${this._settingsManager.namespace}:config`]: this.onConfigCommand.bind(this), - [`start:${this._settingsManager.namespace}:resources`]: this.start.bind(this), - [`stop:${this._settingsManager.namespace}:resources`]: this.stop.bind(this), - [`restart:${this._settingsManager.namespace}:all`]: this.onFinishCommand.bind( - this, - 'SIGINT' - ), - }; - } else { - return undefined; - } - } - /** Return the health information from the observability instance */ - private readonly onHealthCommand = async (): Promise => { - return this._observability.health; - }; - /** Return the service stats from the observability instance */ - private readonly onStatsCommand = async (): Promise => { - return this._observability.metrics; - }; - /** Return the errors stored in the registry from the observability instance */ - private readonly onErrorsCommand = async (): Promise => { - return deCycle(this._observability.errors); - }; - /** Return the custom settings from the configuration manager */ - private readonly onConfigCommand = async (): Promise> => { - return this.settings; - }; - /** - * Perform the finish of the service engine and exit the process - * @param signal - The signal received - */ - private readonly onFinishCommand = async (signal: string): Promise => { - // Stryker disable next-line all - this._logger.warn(`Received ${signal} signal, finishing application engine ...`); - try { - await this.shutdown(); - await this.stop(); - } catch (rawError) { - const cause = Crash.from(rawError); - this._logger.crash(cause); - } finally { - // Stryker disable next-line all - this._logger.info(`Application engine finished`); - setTimeout(process.exit, SHUTDOWN_DELAY, signal === 'SIGINT' ? 0 : 1); - } - }; - /** Handle the command event from the OpenC2 consumer */ - private readonly onCommandEvent = (command: CommandJobHandler): void => { - this.emit('command', command); - }; - /** - * Wrap the start method of the resource to avoid errors - * @param resource - the resource to be wrapped - */ - private readonly wrappedStart = async (resource: Layer.Observable): Promise => { - if ('start' in resource && typeof resource.start === 'function') { - await retryBind(resource.start, resource, [], this.retryOptions); - } else { - // Stryker disable next-line all - this._logger.info(`${resource.name} has not a start method`); - await Promise.resolve(); - } - }; - /** - * Wrap the stop method of the resource to avoid errors - * @param resource - the resource to be wrapped - * @returns - */ - private readonly wrappedStop = async (resource: Layer.Observable): Promise => { - if ('stop' in resource && typeof resource.stop === 'function') { - await retryBind(resource.stop, resource, [], this.retryOptions); - } else { - // Stryker disable next-line all - this._logger.info(`${resource.name} has not a stop method`); - await Promise.resolve(); - } - }; - /** Perform the bootstrap of all the service registry resources */ - private readonly bootstrap = async (): Promise => { - try { - if (this._booted) { - return; - } - // Stryker disable next-line all - this._logger.info( - `Welcome to ${this.health.name} - ${this._settingsManager.release}, running with instanceId: [${this._settingsManager.instanceId}]` - ); - // Stryker disable next-line all - this._logger.info('Bootstrapping application engine ...'); - await retryBind(this._observability.start, this._observability, [], this.retryOptions); - await retryBind(this._settingsManager.start, this._settingsManager, [], this.retryOptions); - const links = JSON.stringify(this._observability.links, null, 2); - // Stryker disable next-line all - this._logger.info(`Observability engine started, the health information is at: ${links}`); - if (this._consumer.instance) { - await retryBind(this._consumer.start, this._consumer, [], this.retryOptions); - // Stryker disable next-line all - this._logger.info('OpenC2 Consumer engine started'); - } - this._booted = true; - } catch (rawError) { - const cause = Crash.from(rawError); - const error = new Crash(`Error bootstrapping the application engine: ${cause.message}`, { - cause, - }); - this._logger.crash(error); - throw error; - } - }; - /** Perform the shutdown of all the service registry resources */ - private readonly shutdown = async (): Promise => { - try { - if (!this._booted) { - return; - } - // Stryker disable next-line all - this._logger.info('Shutting down application engine ...'); - if (this._consumer.instance) { - await retryBind(this._consumer.stop, this._consumer, [], this.retryOptions); - // Stryker disable next-line all - this._logger.info('OpenC2 Consumer engine stopped'); - } - await retryBind(this._observability.stop, this._observability, [], this.retryOptions); - await retryBind(this._settingsManager.stop, this._settingsManager, [], this.retryOptions); - // Stryker disable next-line all - this._logger.info('Observability engine stopped'); - this._booted = false; - } catch (rawError) { - const cause = Crash.from(rawError); - const error = new Crash(`Error shutting down the application engine: ${cause.message}`, { - cause, - }); - // Stryker disable next-line all - this._logger.crash(error); - throw error; - } - }; - /** @returns The retry options used for starting resources and service */ - private get retryOptions(): RetryOptions | undefined { - return { - logger: this._logger.crash, - ...this._settingsManager.retryOptions, - }; - } - /** @returns Service Register health information */ - public get errors(): ErrorRecord[] { - return this._observability.errors; - } - /** @returns Service Register health information */ - public get health(): Layer.App.Health { - return this._observability.health; - } - /** @returns Service Register status */ - public get status(): Health.Status { - return this._observability.status; - } - /** @returns Service Register settings */ - public get serviceRegistrySettings(): ServiceRegistrySettings { - return this._settingsManager.serviceRegistrySettings; - } - /** @returns Custom settings */ - public get customSettings(): CustomSettings { - return this._settingsManager.customSettings; - } - /** @returns Service settings */ - public get settings(): ServiceSetting { - return this._settingsManager.settings; - } - /** @returns The logger instance */ - public get logger(): LoggerInstance { - return this._logger; - } - /** - * Register a resource within the service observability - * @param resource - The resource or resources to be register - */ - public register(resource: Layer.Observable | Layer.Observable[]): void { - const resources = Array.isArray(resource) ? resource : [resource]; - for (const entry of resources) { - this._resources.push(entry); - // Stryker disable next-line all - this._logger.debug(`Registering resource: ${entry.name}`); - this._observability.attach(entry); - } - } - /** - * Gets the value at path of object. If the resolved value is undefined, the defaultValue is - * returned in its place. - * @param path - path to the property to get - * @param defaultValue - default value to return if the property is not found - * @template T - Type of the property to return - */ - public get(path: string | string[], defaultValue?: T): T | undefined; - /** - * Gets the value at path of object. If the resolved value is undefined, the defaultValue is - * returned in its place. - * @param key - path to the property to get - * @param defaultValue - default value to return if the property is not found - */ - public get

                          ( - key: P, - defaultValue?: CustomSettings[P] - ): CustomSettings[P] | undefined; - public get(path: string | string[], defaultValue?: CustomSetting): T | undefined { - return this._settingsManager.customRegisterConfigManager.get(path, defaultValue); - } - /** Perform the initialization of all the service resources that has been attached */ - public readonly start = async (): Promise => { - const startedResources: Layer.Observable[] = []; - try { - if (this._started) { - return; - } - if (!this._booted) { - await this.bootstrap(); - } - if (this._settingsManager.isPrimary) { - // Stryker disable next-line all - this._logger.info('Application resources are not started in the primary cluster node'); - this._started = true; - return; - } - // Stryker disable next-line all - this._logger.info('Starting application resources ...'); - for (const resource of this._resources) { - // Stryker disable next-line all - this._logger.info(`Starting resource: ${resource.name} ...`); - await this.wrappedStart(resource); - // Stryker disable next-line all - this._logger.info(`... ${resource.name} started`); - startedResources.push(resource); - } - // Stryker disable next-line all - this._logger.info('... application resources started'); - this._started = true; - } catch (rawError) { - const cause = Crash.from(rawError); - const error = new Crash(`Error starting the application resources: ${cause.message}`, { - cause, - }); - this._logger.crash(error); - // Stryker disable next-line all - this._logger.info('Rolling back started resources ...'); - for (const resource of startedResources) { - // Stryker disable next-line all - this._logger.info(`Stopping resource: ${resource.name} ...`); - await this.wrappedStop(resource); - // Stryker disable next-line all - this._logger.info(`... ${resource.name} stopped`); - } - throw error; - } - }; - /** Perform the stop of all the service resources that has been attached */ - public readonly stop = async (): Promise => { - try { - if (!this._started) { - return; - } - if (this._booted) { - await this.shutdown(); - } - if (this._settingsManager.isPrimary) { - // Stryker disable next-line all - this._logger.info('Application resources are not stopped in the primary cluster node'); - this._started = false; - return; - } - // Stryker disable next-line all - this._logger.info('Stopping application resources ...'); - for (const resource of this._resources) { - // Stryker disable next-line all - this._logger.info(`Stopping resource: ${resource.name} ...`); - await this.wrappedStop(resource); - // Stryker disable next-line all - this._logger.info(`... ${resource.name} stopped`); - } - // Stryker disable next-line all - this._logger.info('... application resources stopped'); - this._started = false; - } catch (rawError) { - const cause = Crash.from(rawError); - const error = new Crash(`Error stopping the application resources: ${cause.message}`, { - cause, - }); - // Stryker disable next-line all - this._logger.crash(error); - throw error; - } - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { Logger, LoggerInstance, SetContext } from '@mdf.js/logger'; +import { CommandJobHandler, ResolverMap } from '@mdf.js/openc2'; +import { RetryOptions, deCycle, retryBind } from '@mdf.js/utils'; +import EventEmitter from 'events'; +import { ControlManager } from './control'; +import { ErrorRecord, Metrics, Observability } from './observability'; +import { SettingsManager } from './settings'; +import { + BootstrapOptions, + CustomSetting, + SHUTDOWN_DELAY, + ServiceRegistryOptions, + ServiceRegistrySettings, + ServiceSetting, +} from './types'; + +export declare interface ServiceRegistry { + /** + * Add a listener for the `command` event, emitted when a new command is received + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + on(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Add a listener for the `command` event, emitted when a new command is received + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + addListener(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Add a listener for the `command` event, emitted when a new command is received. This is a + * one-time event, the listener will be removed after the first emission. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + once(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes the specified listener from the listener array for the `command` event. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + off(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes the specified listener from the listener array for the `command` event. + * @param event - `command` event + * @param listener - Command event listener + * @event + */ + removeListener(event: 'command', listener: (job: CommandJobHandler) => void): this; + /** + * Removes all listeners, or those of the specified event. + * @param event - `command` event + */ + removeAllListeners(event?: 'command'): this; +} + +export class ServiceRegistry< + CustomSettings extends Record = Record, +> extends EventEmitter { + /** Service Settings manager */ + private readonly _settingsManager: SettingsManager; + /** Resources attached to the service registry observability */ + private readonly _resources: Layer.Observable[] = []; + /** Service Registry observability instance */ + private readonly _observability: Observability; + /** Service Registry control manager */ + private readonly _consumer: ControlManager; + /** Flag to indicate if the service has performed the bootstrap */ + private _booted = false; + /** Flag to indicate if the service has started */ + private _started = false; + /** Logger instance */ + private readonly _logger: LoggerInstance; + /** + * Create a new instance of the Service Registry + * @param bootstrapOptions - Bootstrap settings, define how the Custom and the Service Registry + * settings should be loaded. + * @param serviceRegistryOptions - Service Registry settings, used as a base for the Service + * Registry configuration manager. + * @param customSettings - Custom settings provided by the user, used as a base for the Custom + * configuration manager. + */ + constructor( + bootstrapOptions?: BootstrapOptions, + serviceRegistryOptions?: ServiceRegistryOptions, + customSettings?: Partial + ) { + super(); + this._settingsManager = new SettingsManager( + bootstrapOptions, + serviceRegistryOptions, + customSettings + ); + this._logger = SetContext( + new Logger(this._settingsManager.name, this._settingsManager.logger), + this._settingsManager.name, + this._settingsManager.instanceId + ); + this._observability = new Observability({ + ...this._settingsManager.observability, + logger: this._logger, + }); + this._consumer = new ControlManager( + this._settingsManager.serviceRegistrySettings, + this._logger, + this.resolverMap + ); + this._observability.attach(this._settingsManager); + if (this._consumer.instance && !this._consumer.error && bootstrapOptions?.consumer) { + this._consumer.on('command', this.onCommandEvent.bind(this)); + this._observability.attach(this._consumer.instance); + } else if (this._consumer.error) { + // Stryker disable next-line all + this._logger.warn( + 'OpenC2 Consumer is not available, the service is not able to receive commands' + ); + this._observability.push(this._consumer.error); + } + process.on('SIGINT', () => this.onFinishCommand('SIGINT')); + process.on('SIGTERM', () => this.onFinishCommand('SIGTERM')); + } + /** @returns Default resolver map for the OpenC2 Consumer interface */ + private get resolverMap(): ResolverMap | undefined { + if (this._settingsManager.namespace) { + return { + [`query:${this._settingsManager.namespace}:health`]: this.onHealthCommand.bind(this), + [`query:${this._settingsManager.namespace}:stats`]: this.onStatsCommand.bind(this), + [`query:${this._settingsManager.namespace}:errors`]: this.onErrorsCommand.bind(this), + [`query:${this._settingsManager.namespace}:config`]: this.onConfigCommand.bind(this), + [`start:${this._settingsManager.namespace}:resources`]: this.start.bind(this), + [`stop:${this._settingsManager.namespace}:resources`]: this.stop.bind(this), + [`restart:${this._settingsManager.namespace}:all`]: this.onFinishCommand.bind( + this, + 'SIGINT' + ), + }; + } else { + return undefined; + } + } + /** Return the health information from the observability instance */ + private readonly onHealthCommand = async (): Promise => { + return this._observability.health; + }; + /** Return the service stats from the observability instance */ + private readonly onStatsCommand = async (): Promise => { + return this._observability.metrics; + }; + /** Return the errors stored in the registry from the observability instance */ + private readonly onErrorsCommand = async (): Promise => { + return deCycle(this._observability.errors); + }; + /** Return the custom settings from the configuration manager */ + private readonly onConfigCommand = async (): Promise> => { + return this.settings; + }; + /** + * Perform the finish of the service engine and exit the process + * @param signal - The signal received + */ + private readonly onFinishCommand = async (signal: string): Promise => { + // Stryker disable next-line all + this._logger.warn(`Received ${signal} signal, finishing application engine ...`); + try { + await this.shutdown(); + await this.stop(); + } catch (rawError) { + const cause = Crash.from(rawError); + this._logger.crash(cause); + } finally { + // Stryker disable next-line all + this._logger.info(`Application engine finished`); + setTimeout(process.exit, SHUTDOWN_DELAY, signal === 'SIGINT' ? 0 : 1); + } + }; + /** Handle the command event from the OpenC2 consumer */ + private readonly onCommandEvent = (command: CommandJobHandler): void => { + this.emit('command', command); + }; + /** + * Wrap the start method of the resource to avoid errors + * @param resource - the resource to be wrapped + */ + private readonly wrappedStart = async (resource: Layer.Observable): Promise => { + if ('start' in resource && typeof resource.start === 'function') { + await retryBind(resource.start, resource, [], this.retryOptions); + } else { + // Stryker disable next-line all + this._logger.info(`${resource.name} has not a start method`); + await Promise.resolve(); + } + }; + /** + * Wrap the stop method of the resource to avoid errors + * @param resource - the resource to be wrapped + * @returns + */ + private readonly wrappedStop = async (resource: Layer.Observable): Promise => { + if ('stop' in resource && typeof resource.stop === 'function') { + await retryBind(resource.stop, resource, [], this.retryOptions); + } else { + // Stryker disable next-line all + this._logger.info(`${resource.name} has not a stop method`); + await Promise.resolve(); + } + }; + /** Perform the bootstrap of all the service registry resources */ + private readonly bootstrap = async (): Promise => { + try { + if (this._booted) { + return; + } + // Stryker disable next-line all + this._logger.info(`Welcome to ${this.identification}`); + // Stryker disable next-line all + this._logger.info('Bootstrapping application engine ...'); + await retryBind(this._observability.start, this._observability, [], this.retryOptions); + await retryBind(this._settingsManager.start, this._settingsManager, [], this.retryOptions); + const links = JSON.stringify(this._observability.links, null, 2); + // Stryker disable next-line all + this._logger.info(`Observability engine started, the health information is at: ${links}`); + if (this._consumer.instance) { + await retryBind(this._consumer.start, this._consumer, [], this.retryOptions); + // Stryker disable next-line all + this._logger.info('OpenC2 Consumer engine started'); + } + this._booted = true; + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error bootstrapping the application engine: ${cause.message}`, { + cause, + }); + this._logger.crash(error); + throw error; + } + }; + /** Perform the shutdown of all the service registry resources */ + private readonly shutdown = async (): Promise => { + try { + if (!this._booted) { + return; + } + // Stryker disable next-line all + this._logger.info('Shutting down application engine ...'); + if (this._consumer.instance) { + await retryBind(this._consumer.stop, this._consumer, [], this.retryOptions); + // Stryker disable next-line all + this._logger.info('OpenC2 Consumer engine stopped'); + } + await retryBind(this._observability.stop, this._observability, [], this.retryOptions); + await retryBind(this._settingsManager.stop, this._settingsManager, [], this.retryOptions); + // Stryker disable next-line all + this._logger.info('Observability engine stopped'); + this._booted = false; + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error shutting down the application engine: ${cause.message}`, { + cause, + }); + // Stryker disable next-line all + this._logger.crash(error); + throw error; + } + }; + /** @returns The retry options used for starting resources and service */ + private get retryOptions(): RetryOptions | undefined { + return { + logger: this._logger.crash, + ...this._settingsManager.retryOptions, + }; + } + /** @returns Service Register health information */ + public get errors(): ErrorRecord[] { + return this._observability.errors; + } + /** @returns Service Register health information */ + public get health(): Layer.App.Health { + return this._observability.health; + } + /** @returns Service Register status */ + public get status(): Health.Status { + return this._observability.status; + } + /** @returns Service Register settings */ + public get serviceRegistrySettings(): ServiceRegistrySettings { + return this._settingsManager.serviceRegistrySettings; + } + /** @returns Custom settings */ + public get customSettings(): CustomSettings { + return this._settingsManager.customSettings; + } + /** @returns Service settings */ + public get settings(): ServiceSetting { + return this._settingsManager.settings; + } + /** @returns The logger instance */ + public get logger(): LoggerInstance { + return this._logger; + } + /** @return The application identification string */ + private get identification(): string { + return `${this.health.name} - ${this.health.serviceId}/${this.health.serviceGroupId} - ${this.health.release} - running with instanceId: [${this.health.instanceId}]`; + } + /** + * Register a resource within the service observability + * @param resource - The resource or resources to be register + */ + public register(resource: Layer.Observable | Layer.Observable[]): void { + const resources = Array.isArray(resource) ? resource : [resource]; + for (const entry of resources) { + this._resources.push(entry); + // Stryker disable next-line all + this._logger.debug(`Registering resource: ${entry.name}`); + this._observability.attach(entry); + } + } + /** + * Gets the value at path of object. If the resolved value is undefined, the defaultValue is + * returned in its place. + * @param path - path to the property to get + * @param defaultValue - default value to return if the property is not found + * @template T - Type of the property to return + */ + public get(path: string | string[], defaultValue?: T): T | undefined; + /** + * Gets the value at path of object. If the resolved value is undefined, the defaultValue is + * returned in its place. + * @param key - path to the property to get + * @param defaultValue - default value to return if the property is not found + */ + public get

                          ( + key: P, + defaultValue?: CustomSettings[P] + ): CustomSettings[P] | undefined; + public get(path: string | string[], defaultValue?: CustomSetting): T | undefined { + return this._settingsManager.customRegisterConfigManager.get(path, defaultValue); + } + /** Perform the initialization of all the service resources that has been attached */ + public readonly start = async (): Promise => { + const startedResources: Layer.Observable[] = []; + try { + if (this._started) { + return; + } + if (!this._booted) { + await this.bootstrap(); + } + if (this._settingsManager.isPrimary) { + // Stryker disable next-line all + this._logger.info('Application resources are not started in the primary cluster node'); + this._started = true; + return; + } + // Stryker disable next-line all + this._logger.info('Starting application resources ...'); + for (const resource of this._resources) { + // Stryker disable next-line all + this._logger.info(`Starting resource: ${resource.name} ...`); + await this.wrappedStart(resource); + // Stryker disable next-line all + this._logger.info(`... ${resource.name} started`); + startedResources.push(resource); + } + // Stryker disable next-line all + this._logger.info('... application resources started'); + this._started = true; + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error starting the application resources: ${cause.message}`, { + cause, + }); + this._logger.crash(error); + // Stryker disable next-line all + this._logger.info('Rolling back started resources ...'); + for (const resource of startedResources) { + // Stryker disable next-line all + this._logger.info(`Stopping resource: ${resource.name} ...`); + await this.wrappedStop(resource); + // Stryker disable next-line all + this._logger.info(`... ${resource.name} stopped`); + } + throw error; + } + }; + /** Perform the stop of all the service resources that has been attached */ + public readonly stop = async (): Promise => { + try { + if (!this._started) { + return; + } + if (this._booted) { + await this.shutdown(); + } + if (this._settingsManager.isPrimary) { + // Stryker disable next-line all + this._logger.info('Application resources are not stopped in the primary cluster node'); + this._started = false; + return; + } + // Stryker disable next-line all + this._logger.info('Stopping application resources ...'); + for (const resource of this._resources) { + // Stryker disable next-line all + this._logger.info(`Stopping resource: ${resource.name} ...`); + await this.wrappedStop(resource); + // Stryker disable next-line all + this._logger.info(`... ${resource.name} stopped`); + } + // Stryker disable next-line all + this._logger.info('... application resources stopped'); + this._started = false; + } catch (rawError) { + const cause = Crash.from(rawError); + const error = new Crash(`Error stopping the application resources: ${cause.message}`, { + cause, + }); + // Stryker disable next-line all + this._logger.crash(error); + throw error; + } + }; +} diff --git a/packages/components/service-registry/src/control/ControlManager.ts b/packages/components/service-registry/src/control/ControlManager.ts index a1228a3d..c679bb9e 100644 --- a/packages/components/service-registry/src/control/ControlManager.ts +++ b/packages/components/service-registry/src/control/ControlManager.ts @@ -1,263 +1,264 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import { LoggerInstance } from '@mdf.js/logger'; -import { - CommandJobHandler, - Consumer, - ConsumerOptions, - Control, - Factory, - ResolverMap, -} from '@mdf.js/openc2'; -import cluster from 'cluster'; -import { MergeWithCustomizer, cloneDeep, merge, mergeWith } from 'lodash'; -import { EventEmitter } from 'stream'; -import { ConsumerAdapterOptions, ServiceRegistryOptions } from '../types'; - -/** - * Customizer function used for merging objects with `MergeWith` function. - * @param objValue - The value from the destination object. - * @param srcValue - The value from the source object. - * @returns The merged value or `undefined` if no merge is needed. - */ -const customizer: MergeWithCustomizer = (objValue, srcValue) => { - if (Array.isArray(objValue)) { - return Array.from(new Set(objValue.concat(srcValue)).values()); - } - return undefined; -}; - -/** - * ControlManager handles OpenC2 command and control interactions, serving as the bridge between - * OpenC2 Consumers and the Service. - * It extends EventEmitter to re-emit the events of the OpenC2 Consumer (e.g., command execution, - * errors, and status updates). - */ -export class ControlManager extends EventEmitter { - /** OpenC2 Consumer instance */ - public readonly instance?: Consumer; - /** Validation error, if exist */ - private _error?: Multi; - /** - * Constructor for the ControlManager class. - * @param serviceRegistrySettings - Service Registry settings, which include the consumer and - * adapter configurations. - * @param logger - Logger instance. - * @param defaultResolver - This is the default resolver map for the OpenC2 interface, which is - * merged with the resolver map from the service registry settings, if provided. The default - * value, passed from the Service Registry instance include resolvers for the `features`: - * - `query`: `health`, `stats`, `errors` and `config` - * - `start`: `resources` - * - `stop`: `resources` - */ - constructor( - private readonly serviceRegistrySettings: ServiceRegistryOptions, - private readonly logger: LoggerInstance, - private readonly defaultResolver?: ResolverMap - ) { - super(); - if (cluster.isWorker) { - // Stryker disable next-line all - this.logger.debug(`The OpenC2 Consumer can not be instantiated in a worker process`); - return; - } - this.instance = this.initializeOpenC2Consumer(); - if (this.instance) { - this.wrapConsumerEvents(this.instance); - } - } - /** - * Instantiates the OpenC2 Consumer instance based on the service registry settings. - * @returns OpenC2 Consumer instance, if it was successfully instantiated. - */ - private initializeOpenC2Consumer(): Consumer | undefined { - const _consumerOptions = this.getConsumerOptions( - this.serviceRegistrySettings, - this.defaultResolver - ); - const _adapterOptions = this.getAdapterOptions(this.serviceRegistrySettings.adapterOptions); - if (_consumerOptions) { - if (_adapterOptions && _adapterOptions.type === 'socketIO') { - // Stryker disable next-line all - this.logger.info( - `A OpenC2 Consumer [${_consumerOptions.id}] based on SocketIO is going to be instantiated.` - ); - return Factory.Consumer.SocketIO(_consumerOptions, _adapterOptions.config); - } else if (_adapterOptions && _adapterOptions.type === 'redis') { - // Stryker disable next-line all - this.logger.info( - `A OpenC2 Consumer [${_consumerOptions.id}] based on Redis is going to be instantiated.` - ); - return Factory.Consumer.Redis(_consumerOptions, _adapterOptions.config); - } else { - // Stryker disable next-line all - this.logger.warn( - `The OpenC2 Consumer will be instantiated with a dummy adapter, no adapter options were provided.` - ); - return Factory.Consumer.Dummy(_consumerOptions); - } - } else if (this._error) { - // Stryker disable next-line all - this.logger.warn( - `The OpenC2 Consumer was not instantiated due to the following errors: ${this._error.trace()}` - ); - } else { - // Stryker disable next-line all - this.logger.debug(`A OpenC2 Consumer was not instantiated`); - } - return undefined; - } - /** - * Returns the validation error, if exist. - * @returns Multi error, if exist. - */ - public get error(): Multi | undefined { - return this._error; - } - /** - * Checks and return a validated version of adapter options. - * @param options - Consumer adapter options - * @returns OpenC2 adapter options, retrieved from the service registry settings, if provided. - */ - private getAdapterOptions(options?: ConsumerAdapterOptions): ConsumerAdapterOptions | undefined { - let _options: ConsumerAdapterOptions | undefined; - if (!options) { - this.logger.warn( - `No consumer adapter options were provided, a dummy adapter will be created` - ); - } else if ( - typeof options.type === 'string' && - options.type !== 'redis' && - options.type !== 'socketIO' - ) { - this.addError(new Crash(`Unknown consumer adapter type, costumer will not be instantiated.`)); - } else { - _options = options; - } - return _options; - } - /** - * Checks and return a validated version of consumer options. - * @param options - Service registry settings - * @param defaultResolver - Default resolver map for the OpenC2 interface - * @returns OpenC2 consumer options, retrieved from the service registry settings, if provided and - * merged with some default options. - */ - private getConsumerOptions( - options: ServiceRegistryOptions, - defaultResolver?: ResolverMap - ): ConsumerOptions | undefined { - const _id = options.consumerOptions?.id || options.metadata?.name; - const _resolver = merge(defaultResolver, options.consumerOptions?.resolver); - const _actionTargetPairs = this.getActionTargetPairs(options); - const _logger = this.logger; - if (!_id) { - this.addError( - new Crash( - `No consumer id was provided in the service registry settings, this looks looks like an internal library error.` - ) - ); - return undefined; - } - return merge(cloneDeep(options.consumerOptions), { - id: _id, - resolver: _resolver, - actionTargetPairs: _actionTargetPairs, - logger: _logger, - }); - } - /** - * Constructs and merges the action-target pairs for the OpenC2 interface based on the namespace - * and service registry settings. - * @param options - Service registry settings - * @returns Action-Target pairs for the OpenC2 interface, merged with the default pairs for the - */ - private getActionTargetPairs(options: ServiceRegistryOptions): Control.ActionTargetPairs { - let _defaultPairs: Control.ActionTargetPairs; - // If a namespace is defined, create pairs for service control by the Service Registry - if (options.metadata?.namespace) { - _defaultPairs = { - query: [ - 'features', - `${options.metadata.namespace}:health`, - `${options.metadata.namespace}:stats`, - `${options.metadata.namespace}:errors`, - ], - start: [`${options.metadata.namespace}:resources`], - stop: [`${options.metadata.namespace}:resources`], - }; - } - // If action-target pairs are provided in the service registry settings, use them - else if (options.consumerOptions?.actionTargetPairs) { - _defaultPairs = {}; - } - // Otherwise, use the default pairs for the OpenC2 interface - else { - _defaultPairs = { query: ['features'] }; - } - return mergeWith(_defaultPairs, options.consumerOptions?.actionTargetPairs, customizer); - } - /** - * Adds an error to the validation error list, creating a new Multi error if necessary. If the - * error is a Multi error, its causes are added to the list. - * @param error - The error to add to the validation error list. - */ - private addError(error?: Crash | Multi | Error): void { - if (!error) { - return; - } - if (!this._error) { - this._error = new Multi(`Error in the OpenC2 Consumer instance configuration`); - } - if (error instanceof Multi) { - if (error.causes) { - error.causes.forEach(cause => { - this._error?.push(cause); - }); - } else { - this._error.push(error); - } - } else if (error instanceof Crash) { - this._error.push(error); - } else { - this._error.push(Crash.from(error)); - } - } - /** - * Event handler for the `command` event emitted by the OpenC2 Consumer instance. - * @param command - The OpenC2 command job handler. - * @returns void - */ - private onCommandEvent(command: CommandJobHandler): void { - this.logger.debug(`Received command: ${JSON.stringify(command)}`); - this.emit('command', command); - } - /** - * Wraps the OpenC2 Consumer instance events with the ControlManager event handlers. - * @param instance - The OpenC2 Consumer instance. - * @returns void - */ - private wrapConsumerEvents(instance: Consumer): void { - instance.on('command', this.onCommandEvent.bind(this)); - } - /** - * Starts the OpenC2 Consumer instance. - * @returns Promise - */ - public async start(): Promise { - await this.instance?.start(); - } - /** - * Stops the OpenC2 Consumer instance. - * @returns Promise - */ - public async stop(): Promise { - await this.instance?.stop(); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import { LoggerInstance } from '@mdf.js/logger'; +import { + CommandJobHandler, + Consumer, + ConsumerOptions, + Control, + Factory, + ResolverMap, +} from '@mdf.js/openc2'; +import cluster from 'cluster'; +import { MergeWithCustomizer, cloneDeep, merge, mergeWith } from 'lodash'; +import { EventEmitter } from 'stream'; +import { ConsumerAdapterOptions, ServiceRegistryOptions } from '../types'; + +/** + * Customizer function used for merging objects with `MergeWith` function. + * @param objValue - The value from the destination object. + * @param srcValue - The value from the source object. + * @returns The merged value or `undefined` if no merge is needed. + */ +const customizer: MergeWithCustomizer = (objValue, srcValue) => { + if (Array.isArray(objValue)) { + return Array.from(new Set(objValue.concat(srcValue)).values()); + } + return undefined; +}; + +/** + * ControlManager handles OpenC2 command and control interactions, serving as the bridge between + * OpenC2 Consumers and the Service. + * It extends EventEmitter to re-emit the events of the OpenC2 Consumer (e.g., command execution, + * errors, and status updates). + */ +export class ControlManager extends EventEmitter { + /** OpenC2 Consumer instance */ + public readonly instance?: Consumer; + /** Validation error, if exist */ + private _error?: Multi; + /** + * Constructor for the ControlManager class. + * @param serviceRegistrySettings - Service Registry settings, which include the consumer and + * adapter configurations. + * @param logger - Logger instance. + * @param defaultResolver - This is the default resolver map for the OpenC2 interface, which is + * merged with the resolver map from the service registry settings, if provided. The default + * value, passed from the Service Registry instance include resolvers for the `features`: + * - `query`: `health`, `stats`, `errors` and `config` + * - `start`: `resources` + * - `stop`: `resources` + */ + constructor( + private readonly serviceRegistrySettings: ServiceRegistryOptions, + private readonly logger: LoggerInstance, + private readonly defaultResolver?: ResolverMap + ) { + super(); + if (cluster.isWorker) { + // Stryker disable next-line all + this.logger.debug(`The OpenC2 Consumer can not be instantiated in a worker process`); + return; + } + this.instance = this.initializeOpenC2Consumer(); + if (this.instance) { + this.wrapConsumerEvents(this.instance); + } + } + /** + * Instantiates the OpenC2 Consumer instance based on the service registry settings. + * @returns OpenC2 Consumer instance, if it was successfully instantiated. + */ + private initializeOpenC2Consumer(): Consumer | undefined { + const _consumerOptions = this.getConsumerOptions( + this.serviceRegistrySettings, + this.defaultResolver + ); + const _adapterOptions = this.getAdapterOptions(this.serviceRegistrySettings.adapterOptions); + if (_consumerOptions) { + if (_adapterOptions && _adapterOptions.type === 'socketIO') { + // Stryker disable next-line all + this.logger.info( + `A OpenC2 Consumer [${_consumerOptions.id}] based on SocketIO is going to be instantiated.` + ); + return Factory.Consumer.SocketIO(_consumerOptions, _adapterOptions.config); + } else if (_adapterOptions && _adapterOptions.type === 'redis') { + // Stryker disable next-line all + this.logger.info( + `A OpenC2 Consumer [${_consumerOptions.id}] based on Redis is going to be instantiated.` + ); + return Factory.Consumer.Redis(_consumerOptions, _adapterOptions.config); + } else { + // Stryker disable next-line all + this.logger.warn( + `The OpenC2 Consumer will be instantiated with a dummy adapter, no adapter options were provided.` + ); + return Factory.Consumer.Dummy(_consumerOptions); + } + } else if (this._error) { + // Stryker disable next-line all + this.logger.warn( + `The OpenC2 Consumer was not instantiated due to the following errors: ${this._error.trace()}` + ); + } else { + // Stryker disable next-line all + this.logger.debug(`A OpenC2 Consumer was not instantiated`); + } + return undefined; + } + /** + * Returns the validation error, if exist. + * @returns Multi error, if exist. + */ + public get error(): Multi | undefined { + return this._error; + } + /** + * Checks and return a validated version of adapter options. + * @param options - Consumer adapter options + * @returns OpenC2 adapter options, retrieved from the service registry settings, if provided. + */ + private getAdapterOptions(options?: ConsumerAdapterOptions): ConsumerAdapterOptions | undefined { + let _options: ConsumerAdapterOptions | undefined; + if (!options) { + this.logger.warn( + `No consumer adapter options were provided, a dummy adapter will be created` + ); + } else if ( + typeof options.type === 'string' && + options.type !== 'redis' && + options.type !== 'socketIO' + ) { + this.addError(new Crash(`Unknown consumer adapter type, costumer will not be instantiated.`)); + } else { + _options = options; + } + return _options; + } + /** + * Checks and return a validated version of consumer options. + * @param options - Service registry settings + * @param defaultResolver - Default resolver map for the OpenC2 interface + * @returns OpenC2 consumer options, retrieved from the service registry settings, if provided and + * merged with some default options. + */ + private getConsumerOptions( + options: ServiceRegistryOptions, + defaultResolver?: ResolverMap + ): ConsumerOptions | undefined { + const _id = options.consumerOptions?.id ?? options.metadata?.name; + const _resolver = merge(defaultResolver, options.consumerOptions?.resolver); + const _actionTargetPairs = this.getActionTargetPairs(options); + const _logger = this.logger; + if (!_id) { + this.addError( + new Crash( + `No consumer id was provided in the service registry settings, this looks looks like an internal library error.` + ) + ); + return undefined; + } + return merge(cloneDeep(options.consumerOptions), { + id: _id, + resolver: _resolver, + actionTargetPairs: _actionTargetPairs, + logger: _logger, + }); + } + /** + * Constructs and merges the action-target pairs for the OpenC2 interface based on the namespace + * and service registry settings. + * @param options - Service registry settings + * @returns Action-Target pairs for the OpenC2 interface, merged with the default pairs for the + */ + private getActionTargetPairs(options: ServiceRegistryOptions): Control.ActionTargetPairs { + let _defaultPairs: Control.ActionTargetPairs; + // If a namespace is defined, create pairs for service control by the Service Registry + if (options.metadata?.namespace) { + _defaultPairs = { + query: [ + 'features', + `${options.metadata.namespace}:health`, + `${options.metadata.namespace}:stats`, + `${options.metadata.namespace}:errors`, + ], + start: [`${options.metadata.namespace}:resources`], + stop: [`${options.metadata.namespace}:resources`], + }; + } + // If action-target pairs are provided in the service registry settings, use them + else if (options.consumerOptions?.actionTargetPairs) { + _defaultPairs = {}; + } + // Otherwise, use the default pairs for the OpenC2 interface + else { + _defaultPairs = { query: ['features'] }; + } + return mergeWith(_defaultPairs, options.consumerOptions?.actionTargetPairs, customizer); + } + /** + * Adds an error to the validation error list, creating a new Multi error if necessary. If the + * error is a Multi error, its causes are added to the list. + * @param error - The error to add to the validation error list. + */ + private addError(error?: Crash | Multi | Error): void { + if (!error) { + return; + } + if (!this._error) { + this._error = new Multi(`Error in the OpenC2 Consumer instance configuration`); + } + if (error instanceof Multi) { + if (error.causes) { + error.causes.forEach(cause => { + this._error?.push(cause); + }); + } else { + this._error.push(error); + } + } else if (error instanceof Crash) { + this._error.push(error); + } else { + this._error.push(Crash.from(error)); + } + } + /** + * Event handler for the `command` event emitted by the OpenC2 Consumer instance. + * @param command - The OpenC2 command job handler. + * @returns void + */ + private onCommandEvent(command: CommandJobHandler): void { + this.logger.debug(`Received command: ${JSON.stringify(command)}`); + this.emit('command', command); + } + /** + * Wraps the OpenC2 Consumer instance events with the ControlManager event handlers. + * @param instance - The OpenC2 Consumer instance. + * @returns void + */ + private wrapConsumerEvents(instance: Consumer): void { + instance.on('command', this.onCommandEvent.bind(this)); + } + /** + * Starts the OpenC2 Consumer instance. + * @returns Promise + */ + public async start(): Promise { + await this.instance?.start(); + } + /** + * Stops the OpenC2 Consumer instance. + * @returns Promise + */ + public async stop(): Promise { + await this.instance?.stop(); + } +} + diff --git a/packages/components/service-registry/src/index.ts b/packages/components/service-registry/src/index.ts index 32592a44..c58e25b3 100644 --- a/packages/components/service-registry/src/index.ts +++ b/packages/components/service-registry/src/index.ts @@ -1,22 +1,28 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Health, Layer } from '@mdf.js/core'; -export { LoggerConfig, LoggerInstance } from '@mdf.js/logger'; -export { CommandJobHandler, ConsumerOptions, Control, ResolverMap } from '@mdf.js/openc2'; -export { Setup } from '@mdf.js/service-setup-provider'; -export { RetryOptions } from '@mdf.js/utils'; -export { ServiceRegistry } from './ServiceRegistry'; -export { ErrorRecord, ObservabilityServiceOptions } from './observability'; -export type { - ConsumerAdapterOptions, - CustomSetting, - CustomSettings, - ServiceRegistryOptions, - ServiceRegistrySettings, - ServiceSetting, -} from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Health, Layer } from '@mdf.js/core'; +export { LoggerConfig, LoggerInstance } from '@mdf.js/logger'; +export { CommandJobHandler, ConsumerOptions, Control, ResolverMap } from '@mdf.js/openc2'; +export { Setup } from '@mdf.js/service-setup-provider'; +export { RetryOptions } from '@mdf.js/utils'; +export { + ErrorRecord, + ExtendedCrashObject, + ExtendedMultiObject, + ObservabilityServiceOptions, +} from './observability'; +export { ServiceRegistry } from './ServiceRegistry'; +export type { + BootstrapOptions, + ConsumerAdapterOptions, + CustomSetting, + CustomSettings, + ServiceRegistryOptions, + ServiceRegistrySettings, + ServiceSetting, +} from './types'; diff --git a/packages/components/service-registry/src/observability/Observability.ts b/packages/components/service-registry/src/observability/Observability.ts index ac96a3dd..d0084340 100644 --- a/packages/components/service-registry/src/observability/Observability.ts +++ b/packages/components/service-registry/src/observability/Observability.ts @@ -1,179 +1,180 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { Crash, Links, Multi } from '@mdf.js/crash'; -import cluster from 'cluster'; -import { ObservabilityAppManager } from './ObservabilityAppManager'; -import { ErrorRecord, ErrorRegistry, HealthRegistry, Metrics, MetricsRegistry } from './registries'; -import { ObservabilityOptions } from './types'; - -/** - * Represents a comprehensive observability service that aggregates various registries - * including health checks, metrics, and error logging. This class is responsible for - * managing and initializing these registries, attaching services to them, and integrating - * them into a unified observability application. - * - * The service leverages an `ObservabilityAppManager` to orchestrate the express application - * that serves observability endpoints. It allows for dynamic registration of services - * to enable monitoring, health checks, and error tracking. - */ -export class Observability { - /** Manages the Express application dedicated to observability features. */ - private readonly _app: ObservabilityAppManager; - /** Collection of registries (Health, Metrics, Errors) utilized by observability. */ - private readonly _registers: Layer.App.Service[]; - /** Central registry for capturing and reporting errors across services. */ - private readonly _errorsRegistry: ErrorRegistry; - /** Central registry for collecting and exposing metrics from services. */ - private readonly _metricsRegistry: MetricsRegistry; - /** Central registry for collecting and exposing metrics from services. */ - private readonly _healthRegistry: HealthRegistry; - /** - * Initializes the observability service with specified options, setting up - * health, metrics, and error registries based on those options. - * @param options - Configuration options for observability, including settings - * for health checks, metrics collection, and error logging. - */ - constructor(public readonly options: ObservabilityOptions) { - this._healthRegistry = new HealthRegistry({ - applicationMetadata: this.options.metadata, - isCluster: this.options.service?.isCluster, - clusterUpdateInterval: this.options.service?.clusterUpdateInterval, - logger: this.options.logger, - }); - this._errorsRegistry = new ErrorRegistry({ - name: this.options.metadata.name, - instanceId: this.options.metadata.instanceId, - maxSize: this.options.service?.maxSize, - includeStack: this.options.service?.includeStack, - isCluster: this.options.service?.isCluster, - clusterUpdateInterval: this.options.service?.clusterUpdateInterval, - logger: options.logger, - }); - this._metricsRegistry = new MetricsRegistry({ - name: this.options.metadata.name, - instanceId: this.options.metadata.instanceId, - isCluster: this.options.service?.isCluster, - logger: this.options.logger, - }); - this._registers = [this._healthRegistry, this._metricsRegistry, this._errorsRegistry]; - this._app = new ObservabilityAppManager(this.options, this._metricsRegistry.registry); - } - /** - * Attaches a new service to be monitored under the observability framework. - * Services are components of your application that you wish to monitor for - * health, track errors for, and collect metrics on. - * @param observable - The service to attach to observability. - */ - public attach(observable: Layer.Observable): void { - // Add the service that can emit errors to the errors registry - this._errorsRegistry.register(observable); - // Add the service that can collect metrics to the metrics registry - this._metricsRegistry.register(observable as Layer.App.Service); - // Add the service with health information to the health registry - this._healthRegistry.register(observable as Layer.App.Service); - // Add the service with REST API interface in the observability app - this._app.register(observable as Layer.App.Service); - } - /** Start the observability service */ - public async start(): Promise { - if (this._app.isBuild) { - return; - } - // If the service is running in a cluster or standalone, register the errors summary - if (cluster.isPrimary) { - this._healthRegistry.register(this._errorsRegistry); - } - // Register all the services in the observability app - this._app.register(this._registers); - - // Start all the registers - for (const register of this._registers) { - await register.start(); - } - this._app.build(); - await this._app.start(); - } - /** Stop the observability service */ - public async stop(): Promise { - if (!this._app.isBuild) { - return; - } - await this._app.stop(); - this._app.unbuilt(); - for (const register of this._registers) { - await register.stop(); - } - } - /** Close the observability service */ - public async close(): Promise { - if (!this._app.isBuild) { - return; - } - await this.stop(); - for (const register of this._registers) { - if (typeof register.close === 'function') { - await register.close.call(register); - } - } - } - /** - * Adds an error to the registry, converting it to a structured format. - * @param error - The error to register. - */ - public push(error: Crash | Multi | Error): void { - this._errorsRegistry.push(error); - } - /** - * Adds a timestamped note to the health status. - * @param note - Note to be added. - */ - public addNote(note: string): void { - this._healthRegistry.addNote(note); - } - /** - * Update or add a check measure. - * This should be used to inform about the state of resources behind the Component/Microservice, - * for example states of connections with field devices. - * - * The new check will be taking into account in the overall health status. - * The new check will be included in the `checks` object with the key "component:measure". - * If this key already exists, the `componentId` of the `check` parameter will be checked, if - * there is a check with the same `componentId` in the array, the check will be updated, in other - * case the new check will be added to the existing array. - * - * The maximum number external checks is 100 - * @param component - component identification - * @param measure - measure identification - * @param check - check to be updated or included - * @returns true, if the check has been updated - */ - public addCheck(component: string, measure: string, check: Health.Check): boolean { - return this._healthRegistry.addCheck(component, measure, check); - } - /** @returns The health of the monitored application */ - public get health(): Layer.App.Health { - return this._healthRegistry.health; - } - /** @returns The status of the monitored application */ - public get status(): Health.Status { - return this._healthRegistry.status; - } - /** @returns The metrics of the monitored application */ - public get metrics(): Promise { - return this._metricsRegistry.metricsJSON(); - } - /** @returns The errors of the monitored application */ - public get errors(): ErrorRecord[] { - return this._errorsRegistry.errors; - } - /** @returns The observability rest api access end points */ - public get links(): Links { - return this._app.links; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { Crash, Links, Multi } from '@mdf.js/crash'; +import cluster from 'cluster'; +import { ObservabilityAppManager } from './ObservabilityAppManager'; +import { ErrorRecord, ErrorRegistry, HealthRegistry, Metrics, MetricsRegistry } from './registries'; +import { ObservabilityOptions } from './types'; + +/** + * Represents a comprehensive observability service that aggregates various registries + * including health checks, metrics, and error logging. This class is responsible for + * managing and initializing these registries, attaching services to them, and integrating + * them into a unified observability application. + * + * The service leverages an `ObservabilityAppManager` to orchestrate the express application + * that serves observability endpoints. It allows for dynamic registration of services + * to enable monitoring, health checks, and error tracking. + */ +export class Observability { + /** Manages the Express application dedicated to observability features. */ + private readonly _app: ObservabilityAppManager; + /** Collection of registries (Health, Metrics, Errors) utilized by observability. */ + private readonly _registers: Layer.App.Service[]; + /** Central registry for capturing and reporting errors across services. */ + private readonly _errorsRegistry: ErrorRegistry; + /** Central registry for collecting and exposing metrics from services. */ + private readonly _metricsRegistry: MetricsRegistry; + /** Central registry for collecting and exposing metrics from services. */ + private readonly _healthRegistry: HealthRegistry; + /** + * Initializes the observability service with specified options, setting up + * health, metrics, and error registries based on those options. + * @param options - Configuration options for observability, including settings + * for health checks, metrics collection, and error logging. + */ + constructor(public readonly options: ObservabilityOptions) { + this._healthRegistry = new HealthRegistry({ + applicationMetadata: this.options.metadata, + isCluster: this.options.service?.isCluster, + clusterUpdateInterval: this.options.service?.clusterUpdateInterval, + logger: this.options.logger, + }); + this._errorsRegistry = new ErrorRegistry({ + name: this.options.metadata.name, + instanceId: this.options.metadata.instanceId, + maxSize: this.options.service?.maxSize, + includeStack: this.options.service?.includeStack, + isCluster: this.options.service?.isCluster, + clusterUpdateInterval: this.options.service?.clusterUpdateInterval, + logger: options.logger, + }); + this._metricsRegistry = new MetricsRegistry({ + name: this.options.metadata.name, + instanceId: this.options.metadata.instanceId, + isCluster: this.options.service?.isCluster, + logger: this.options.logger, + }); + this._registers = [this._healthRegistry, this._metricsRegistry, this._errorsRegistry]; + this._app = new ObservabilityAppManager(this.options, this._metricsRegistry.registry); + } + /** + * Attaches a new service to be monitored under the observability framework. + * Services are components of your application that you wish to monitor for + * health, track errors for, and collect metrics on. + * @param observable - The service to attach to observability. + */ + public attach(observable: Layer.Observable): void { + // Add the service that can emit errors to the errors registry + this._errorsRegistry.register(observable); + // Add the service that can collect metrics to the metrics registry + this._metricsRegistry.register(observable as Layer.App.Service); + // Add the service with health information to the health registry + this._healthRegistry.register(observable as Layer.App.Service); + // Add the service with REST API interface in the observability app + this._app.register(observable as Layer.App.Service); + } + /** Start the observability service */ + public async start(): Promise { + if (this._app.isBuild) { + return; + } + // If the service is running in a cluster or standalone, register the errors summary + if (cluster.isPrimary) { + this._healthRegistry.register(this._errorsRegistry); + } + // Register all the services in the observability app + this._app.register(this._registers); + + // Start all the registers + for (const register of this._registers) { + await register.start(); + } + this._app.build(); + await this._app.start(); + } + /** Stop the observability service */ + public async stop(): Promise { + if (!this._app.isBuild) { + return; + } + await this._app.stop(); + this._app.unbuilt(); + for (const register of this._registers) { + await register.stop(); + } + } + /** Close the observability service */ + public async close(): Promise { + if (!this._app.isBuild) { + return; + } + await this.stop(); + for (const register of this._registers) { + if (typeof register.close === 'function') { + await register.close(); + } + } + } + /** + * Adds an error to the registry, converting it to a structured format. + * @param error - The error to register. + */ + public push(error: Crash | Multi | Error): void { + this._errorsRegistry.push(error); + } + /** + * Adds a timestamped note to the health status. + * @param note - Note to be added. + */ + public addNote(note: string): void { + this._healthRegistry.addNote(note); + } + /** + * Update or add a check measure. + * This should be used to inform about the state of resources behind the Component/Microservice, + * for example states of connections with field devices. + * + * The new check will be taking into account in the overall health status. + * The new check will be included in the `checks` object with the key "component:measure". + * If this key already exists, the `componentId` of the `check` parameter will be checked, if + * there is a check with the same `componentId` in the array, the check will be updated, in other + * case the new check will be added to the existing array. + * + * The maximum number external checks is 100 + * @param component - component identification + * @param measure - measure identification + * @param check - check to be updated or included + * @returns true, if the check has been updated + */ + public addCheck(component: string, measure: string, check: Health.Check): boolean { + return this._healthRegistry.addCheck(component, measure, check); + } + /** @returns The health of the monitored application */ + public get health(): Layer.App.Health { + return this._healthRegistry.health; + } + /** @returns The status of the monitored application */ + public get status(): Health.Status { + return this._healthRegistry.status; + } + /** @returns The metrics of the monitored application */ + public get metrics(): Promise { + return this._metricsRegistry.metricsJSON(); + } + /** @returns The errors of the monitored application */ + public get errors(): ErrorRecord[] { + return this._errorsRegistry.errors; + } + /** @returns The observability rest api access end points */ + public get links(): Links { + return this._app.links; + } +} + diff --git a/packages/components/service-registry/src/observability/ObservabilityAppManager.ts b/packages/components/service-registry/src/observability/ObservabilityAppManager.ts index 4efbcc44..44717000 100644 --- a/packages/components/service-registry/src/observability/ObservabilityAppManager.ts +++ b/packages/components/service-registry/src/observability/ObservabilityAppManager.ts @@ -1,207 +1,210 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Layer } from '@mdf.js/core'; -import { Links } from '@mdf.js/crash'; -import { HTTP } from '@mdf.js/http-server-provider'; -import { Middleware } from '@mdf.js/middlewares'; -import cluster from 'cluster'; -import express, { Express } from 'express'; -import { createProxyMiddleware } from 'http-proxy-middleware'; -import { merge } from 'lodash'; -import { Registry } from 'prom-client'; -import { DEFAULT_PORT, DEFAULT_PRIMARY_PORT, ObservabilityOptions } from './types'; - -/** - * Manages the lifecycle and configuration of an Express application dedicated to observability. - * This includes setting up middleware, routing, and server configuration. - * It also supports dynamic registration of services and links for enhanced observability. - */ -export class ObservabilityAppManager { - /** Express app */ - private _app: express.Express | undefined; - /** Registries router */ - private _router: express.Router; - /** Links offered by application */ - private _links: Links = {}; - /** HTTP server */ - private _server?: HTTP.Provider; - /** Options for the observability service */ - private _options: ObservabilityOptions & { service: { primaryPort: number; port: number } }; - /** - * Create an instance of observability service - * @param options - observability options - * @param registry - registry to be used for endpoints metrics - */ - constructor( - options: ObservabilityOptions, - private readonly registry: Registry - ) { - this._router = express.Router(); - this._options = merge( - { service: { primaryPort: DEFAULT_PRIMARY_PORT, port: DEFAULT_PORT } }, - options - ); - this._options.service.port = this.checkPortInRange(this._options.service.port, DEFAULT_PORT); - this._options.service.primaryPort = this.checkPortInRange( - this._options.service.primaryPort, - DEFAULT_PRIMARY_PORT - ); - } - /** Indicates whether the server has been initialized. */ - public get isBuild(): boolean { - return !!this._server; - } - /** Starts the server if it has been built. */ - public async start(): Promise { - await this._server?.start(); - } - /** Stops the server if it is running. */ - public async stop(): Promise { - await this._server?.stop(); - } - /** Constructs the server with the configured options. */ - public build(): void { - if (this.isBuild) { - return; - } - if (this.isWorker) { - this._app = this.workerApp(); - } else { - this._app = this.primaryApp( - this._router, - this.registry, - this.apiVersion, - Middleware.Default.FormatLinks(this.apiVersion, this._links) - ); - } - this._server = HTTP.Factory.create({ - name: 'observability', - config: { - app: this._app, - port: this.getPort(), - host: this._options.service?.host, - }, - }); - } - /** Resets the server to its initial state. */ - public unbuilt(): void { - this._server = undefined; - this._router = express.Router(); - this._links = {}; - this._app = undefined; - } - /** @returns The links offered by this service */ - public get links(): Links { - return Middleware.Default.FormatLinks( - `${this.baseURL}:${this.getPort()}${this.apiVersion}`, - this._links - ); - } - /** Registers a new service with the observability app. */ - public register(service: Layer.App.Service | Layer.App.Service[]): void { - const _services = Array.isArray(service) ? service : [service]; - for (const service of _services) { - if (typeof service.router !== 'undefined') { - this.addRouter(service.router); - } - if (typeof service.links === 'object') { - this.addLinks(service.links); - } - } - } - /** Get the base url whew the observability is served */ - private get baseURL(): string { - const _attachedAddress = this._server?.client?.address(); - let address: string; - if (_attachedAddress) { - if (typeof _attachedAddress === 'string') { - address = _attachedAddress.split(':')[1]; - } else { - address = _attachedAddress.address; - } - } else { - address = '127.0.0.1'; - } - return `http://${address}`; - } - /** Get the api version */ - private get apiVersion(): string { - return `/v${this._options.metadata.version}`; - } - /** Add a new link to the observability */ - private addLinks(links: Links): void { - this._links = merge(this._links, links); - } - /** Add a new router to the observability */ - private addRouter(router: express.Router): void { - this._router.use(router); - } - /** - * Create an express app that offer all the services routes - * @param router - router to be used - * @param registry - registry to be used for endpoints metrics - * @param apiVersion - api version to be used - * @param defaultLinks - default links to be used - */ - private primaryApp( - router: express.Router, - registry: Registry, - apiVersion: string, - defaultLinks: Links - ): Express { - const app = express(); - app.use(Middleware.RequestId.handler()); - app.use(Middleware.BodyParser.JSONParserHandler()); - app.use(Middleware.Metrics.handler(registry)); - app.use(apiVersion, router); - app.use(Middleware.Default.handler(defaultLinks)); - app.use(Middleware.ErrorHandler.handler()); - return app; - } - /** Create an express app that redirect all the request to the master */ - private workerApp(): Express { - const app = express(); - app.use( - createProxyMiddleware({ - router: () => `${this.baseURL}:${this._options.service.primaryPort}`, - changeOrigin: true, - }) - ); - return app; - } - /** Get if the current process is a worker */ - private get isWorker(): boolean { - return cluster.isWorker; - } - /** Get if the current process is working in cluster mode */ - private get isClusterMode(): boolean { - return typeof this._options.service?.isCluster === 'boolean' - ? this._options.service?.isCluster - : false; - } - /** - * Check if the port is in the range of valid ports - * @param port - port to be used - * @param defaultPort - default port to be used - * @returns The port to be used - */ - private checkPortInRange(port: number | undefined, defaultPort: number): number { - return !port || port < 1 || port > 65535 ? defaultPort : port; - } - /** - * Get the port to be used by the service based on the configuration - * @returns The port to be used - */ - private getPort(): number { - if (this.isClusterMode && cluster.isPrimary) { - return this._options.service.primaryPort; - } else { - return this._options.service.port; - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Layer } from '@mdf.js/core'; +import { Links } from '@mdf.js/crash'; +import { HTTP } from '@mdf.js/http-server-provider'; +import { Middleware } from '@mdf.js/middlewares'; +import cluster from 'cluster'; +import express, { Express } from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { merge } from 'lodash'; +import { Registry } from 'prom-client'; +import { DEFAULT_PORT, DEFAULT_PRIMARY_PORT, ObservabilityOptions } from './types'; + +/** + * Manages the lifecycle and configuration of an Express application dedicated to observability. + * This includes setting up middleware, routing, and server configuration. + * It also supports dynamic registration of services and links for enhanced observability. + */ +export class ObservabilityAppManager { + /** Express app */ + private _app: express.Express | undefined; + /** Registries router */ + private _router: express.Router; + /** Links offered by application */ + private _links: Links = {}; + /** HTTP server */ + private _server?: HTTP.Provider; + /** Options for the observability service */ + private readonly _options: ObservabilityOptions & { + service: { primaryPort: number; port: number }; + }; + /** + * Create an instance of observability service + * @param options - observability options + * @param registry - registry to be used for endpoints metrics + */ + constructor( + options: ObservabilityOptions, + private readonly registry: Registry + ) { + this._router = express.Router(); + this._options = merge( + { service: { primaryPort: DEFAULT_PRIMARY_PORT, port: DEFAULT_PORT } }, + options + ); + this._options.service.port = this.checkPortInRange(this._options.service.port, DEFAULT_PORT); + this._options.service.primaryPort = this.checkPortInRange( + this._options.service.primaryPort, + DEFAULT_PRIMARY_PORT + ); + } + /** Indicates whether the server has been initialized. */ + public get isBuild(): boolean { + return !!this._server; + } + /** Starts the server if it has been built. */ + public async start(): Promise { + await this._server?.start(); + } + /** Stops the server if it is running. */ + public async stop(): Promise { + await this._server?.stop(); + } + /** Constructs the server with the configured options. */ + public build(): void { + if (this.isBuild) { + return; + } + if (this.isWorker) { + this._app = this.workerApp(); + } else { + this._app = this.primaryApp( + this._router, + this.registry, + this.apiVersion, + Middleware.Default.FormatLinks(this.apiVersion, this._links) + ); + } + this._server = HTTP.Factory.create({ + name: 'observability', + config: { + app: this._app, + port: this.getPort(), + host: this._options.service?.host, + }, + }); + } + /** Resets the server to its initial state. */ + public unbuilt(): void { + this._server = undefined; + this._router = express.Router(); + this._links = {}; + this._app = undefined; + } + /** @returns The links offered by this service */ + public get links(): Links { + return Middleware.Default.FormatLinks( + `${this.baseURL}:${this.getPort()}${this.apiVersion}`, + this._links + ); + } + /** Registers a new service with the observability app. */ + public register(service: Layer.App.Service | Layer.App.Service[]): void { + const _services = Array.isArray(service) ? service : [service]; + for (const service of _services) { + if (typeof service.router !== 'undefined') { + this.addRouter(service.router); + } + if (typeof service.links === 'object') { + this.addLinks(service.links); + } + } + } + /** Get the base url whew the observability is served */ + private get baseURL(): string { + const _attachedAddress = this._server?.client?.address(); + let address: string; + if (_attachedAddress) { + if (typeof _attachedAddress === 'string') { + address = _attachedAddress.split(':')[1]; + } else { + address = _attachedAddress.address; + } + } else { + address = '127.0.0.1'; + } + return `http://${address}`; + } + /** Get the api version */ + private get apiVersion(): string { + return `/v${this._options.metadata.version}`; + } + /** Add a new link to the observability */ + private addLinks(links: Links): void { + this._links = merge(this._links, links); + } + /** Add a new router to the observability */ + private addRouter(router: express.Router): void { + this._router.use(router); + } + /** + * Create an express app that offer all the services routes + * @param router - router to be used + * @param registry - registry to be used for endpoints metrics + * @param apiVersion - api version to be used + * @param defaultLinks - default links to be used + */ + private primaryApp( + router: express.Router, + registry: Registry, + apiVersion: string, + defaultLinks: Links + ): Express { + const app = express(); + app.use(Middleware.RequestId.handler()); + app.use(Middleware.BodyParser.JSONParserHandler()); + app.use(Middleware.Metrics.handler(registry)); + app.use(apiVersion, router); + app.use(Middleware.Default.handler(defaultLinks)); + app.use(Middleware.ErrorHandler.handler()); + return app; + } + /** Create an express app that redirect all the request to the master */ + private workerApp(): Express { + const app = express(); + app.use( + createProxyMiddleware({ + router: () => `${this.baseURL}:${this._options.service.primaryPort}`, + changeOrigin: true, + }) + ); + return app; + } + /** Get if the current process is a worker */ + private get isWorker(): boolean { + return cluster.isWorker; + } + /** Get if the current process is working in cluster mode */ + private get isClusterMode(): boolean { + return typeof this._options.service?.isCluster === 'boolean' + ? this._options.service?.isCluster + : false; + } + /** + * Check if the port is in the range of valid ports + * @param port - port to be used + * @param defaultPort - default port to be used + * @returns The port to be used + */ + private checkPortInRange(port: number | undefined, defaultPort: number): number { + return !port || port < 1 || port > 65535 ? defaultPort : port; + } + /** + * Get the port to be used by the service based on the configuration + * @returns The port to be used + */ + private getPort(): number { + if (this.isClusterMode && cluster.isPrimary) { + return this._options.service.primaryPort; + } else { + return this._options.service.port; + } + } +} + diff --git a/packages/components/service-registry/src/observability/index.ts b/packages/components/service-registry/src/observability/index.ts index 52b0f2f3..5a0100b6 100644 --- a/packages/components/service-registry/src/observability/index.ts +++ b/packages/components/service-registry/src/observability/index.ts @@ -1,16 +1,19 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export * from './Observability'; -export { - DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, - DEFAULT_CONFIG_REGISTER_INCLUDE_STACK, - DEFAULT_CONFIG_REGISTER_MAX_LIST_SIZE, - ErrorRecord, - Metrics, -} from './registries'; -export { DEFAULT_PRIMARY_PORT, ObservabilityOptions, ObservabilityServiceOptions } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Observability'; +export { + DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, + DEFAULT_CONFIG_REGISTER_INCLUDE_STACK, + DEFAULT_CONFIG_REGISTER_MAX_LIST_SIZE, + ErrorRecord, + ExtendedCrashObject, + ExtendedMultiObject, + Metrics, +} from './registries'; +export { DEFAULT_PRIMARY_PORT, ObservabilityOptions, ObservabilityServiceOptions } from './types'; + diff --git a/packages/components/service-registry/src/observability/registries/errors/Ports/MasterPort.ts b/packages/components/service-registry/src/observability/registries/errors/Ports/MasterPort.ts index 96199564..0deeec40 100644 --- a/packages/components/service-registry/src/observability/registries/errors/Ports/MasterPort.ts +++ b/packages/components/service-registry/src/observability/registries/errors/Ports/MasterPort.ts @@ -1,172 +1,172 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { LoggerInstance } from '@mdf.js/logger'; -import cluster, { Worker } from 'cluster'; -import { Aggregator } from '../Aggregator'; -import { - DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, - ErrorRecord, - RegisterMessage, - RegisterMessageType, -} from '../types'; -import { Port } from './Port'; - -/** - * MasterPort class manages the collection and aggregation of error records from worker processes - * in a clustered environment. It periodically requests error registries from each worker, - * aggregates the errors, and updates the main aggregator instance with the collected errors. - * - * Inherits from the Port class, utilizing its logging capabilities and defining additional - * mechanisms for inter-process communication and error aggregation specific to the master process. - */ -export class MasterPort extends Port { - /** Request sequence number */ - private requestId: number = 0; - /** Timeout interval handler for master polling */ - private timeInterval?: NodeJS.Timeout; - /** - * Create an instance of errors manager in a master process - * @param aggregator - Aggregator instance to manage the errors - * @param logger - Logger instance for logging activities - * @param interval - interval in milliseconds between each error registry poll from workers. - */ - constructor( - private readonly aggregator: Aggregator, - logger: LoggerInstance, - private readonly interval: number = DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL - ) { - super(logger); - // Stryker disable next-line all - this.logger.debug(`New master port instance created: ${JSON.stringify({ interval })}`); - } - /** - * Starts the process of periodically polling error registries from worker processes. - * Ensures that only one polling mechanism is active at any given time. - */ - public start(): void { - // Stryker disable next-line all - this.logger.debug('Starting errors registry polling in master process'); - if (!this.timeInterval) { - this.onSendRequest(); - this.timeInterval = setInterval(this.onSendRequest, this.interval); - } - } - /** - * Stops the polling of error registries from worker processes and clears the polling interval. - */ - public stop(): void { - // Stryker disable next-line all - this.logger.debug('Stopping errors registry polling in master process'); - if (this.timeInterval) { - clearInterval(this.timeInterval); - this.timeInterval = undefined; - } - } - /** - * Clears all error registries, both in the master and in all connected worker processes. - */ - public clear(): void { - for (const worker of Object.values(this.workers)) { - if (worker && worker.isConnected()) { - // Stryker disable next-line all - this.logger.debug(`Sending an clear register request to worker [${worker.process.pid}]`); - worker.send({ - type: RegisterMessageType.CLR_REQ, - }); - } - } - this.aggregator.clear(); - } - /** - * Sends a request to all worker processes to send their current error registries. - * Handles responses, timeouts, and updates the aggregator with aggregated errors from workers. - */ - private readonly onSendRequest = (): void => { - this.requestId = this.requestId + 1; - if (!Number.isFinite(this.requestId)) { - this.requestId = 0; - } - let pendingResponses = Object.keys(this.workers).length; - let timeOutHandler: NodeJS.Timeout | undefined; - let updatedRegistries: ErrorRecord[] = []; - const onWorkerResponse = (worker: Worker, message: RegisterMessage) => { - if (message.requestId !== this.requestId) { - // Stryker disable next-line all - this.logger.debug( - `Update response from worker [${worker.process.pid}] out of the valid period` - ); - return; - } - if (message.type === RegisterMessageType.RES) { - pendingResponses = pendingResponses - 1; - updatedRegistries = this.mergeErrors(updatedRegistries, worker, message.errors); - if (pendingResponses === 0 && timeOutHandler) { - onFinished(); - } - } - }; - const onTimeOut = () => { - // Stryker disable next-line all - this.logger.debug(`Timeout for update response from workers - ${pendingResponses} pending`); - onFinished(); - }; - const onFinished = () => { - // Stryker disable next-line all - this.logger.debug('Registry update from workers finished'); - cluster.off('message', onWorkerResponse); - if (timeOutHandler) { - clearTimeout(timeOutHandler); - timeOutHandler = undefined; - } - this.aggregator.updateWorkersErrors(updatedRegistries); - }; - timeOutHandler = setTimeout(onTimeOut, this.interval * 0.9); - cluster.on('message', onWorkerResponse); - for (const worker of Object.values(this.workers)) { - if (worker && worker.isConnected()) { - // Stryker disable next-line all - this.logger.debug( - `Sending an errors register update request to worker [${worker.process.pid}]` - ); - worker.send({ - type: RegisterMessageType.REQ, - requestId: this.requestId, - }); - } - } - }; - /** - * Merges the errors received from a worker process into the accumulated error records. - * Adds worker identification details to each error record for traceability. - * @param feed - feed to be merged with the errors from the worker - * @param worker - Worker that emit the errors - * @param workerErrors - errors from the worker - */ - private mergeErrors( - feed: ErrorRecord[], - worker: Worker, - workerErrors: ErrorRecord[] - ): ErrorRecord[] { - return feed.concat( - workerErrors.map(error => { - return { - ...error, - workerPid: worker.process.pid, - workerId: worker.id, - }; - }) - ); - } - /** - * Retrieves a dictionary of currently active worker processes. - * @returns A dictionary of Worker instances indexed by their cluster worker ID. - */ - private get workers(): NodeJS.Dict { - return cluster.workers ? cluster.workers : {}; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { LoggerInstance } from '@mdf.js/logger'; +import cluster, { Worker } from 'cluster'; +import { Aggregator } from '../Aggregator'; +import { + DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, + ErrorRecord, + RegisterMessage, + RegisterMessageType, +} from '../types'; +import { Port } from './Port'; + +/** + * MasterPort class manages the collection and aggregation of error records from worker processes + * in a clustered environment. It periodically requests error registries from each worker, + * aggregates the errors, and updates the main aggregator instance with the collected errors. + * + * Inherits from the Port class, utilizing its logging capabilities and defining additional + * mechanisms for inter-process communication and error aggregation specific to the master process. + */ +export class MasterPort extends Port { + /** Request sequence number */ + private requestId: number = 0; + /** Timeout interval handler for master polling */ + private timeInterval?: NodeJS.Timeout; + /** + * Create an instance of errors manager in a master process + * @param aggregator - Aggregator instance to manage the errors + * @param logger - Logger instance for logging activities + * @param interval - interval in milliseconds between each error registry poll from workers. + */ + constructor( + private readonly aggregator: Aggregator, + logger: LoggerInstance, + private readonly interval: number = DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL + ) { + super(logger); + // Stryker disable next-line all + this.logger.debug(`New master port instance created: ${JSON.stringify({ interval })}`); + } + /** + * Starts the process of periodically polling error registries from worker processes. + * Ensures that only one polling mechanism is active at any given time. + */ + public start(): void { + // Stryker disable next-line all + this.logger.debug('Starting errors registry polling in master process'); + if (!this.timeInterval) { + this.onSendRequest(); + this.timeInterval = setInterval(this.onSendRequest, this.interval); + } + } + /** + * Stops the polling of error registries from worker processes and clears the polling interval. + */ + public stop(): void { + // Stryker disable next-line all + this.logger.debug('Stopping errors registry polling in master process'); + if (this.timeInterval) { + clearInterval(this.timeInterval); + this.timeInterval = undefined; + } + } + /** + * Clears all error registries, both in the master and in all connected worker processes. + */ + public clear(): void { + for (const worker of Object.values(this.workers)) { + if (worker?.isConnected()) { + // Stryker disable next-line all + this.logger.debug(`Sending an clear register request to worker [${worker.process.pid}]`); + worker.send({ + type: RegisterMessageType.CLR_REQ, + }); + } + } + this.aggregator.clear(); + } + /** + * Sends a request to all worker processes to send their current error registries. + * Handles responses, timeouts, and updates the aggregator with aggregated errors from workers. + */ + private readonly onSendRequest = (): void => { + this.requestId = this.requestId + 1; + if (!Number.isFinite(this.requestId)) { + this.requestId = 0; + } + let pendingResponses = Object.keys(this.workers).length; + let timeOutHandler: NodeJS.Timeout | undefined; + let updatedRegistries: ErrorRecord[] = []; + const onWorkerResponse = (worker: Worker, message: RegisterMessage) => { + if (message.requestId !== this.requestId) { + // Stryker disable next-line all + this.logger.debug( + `Update response from worker [${worker.process.pid}] out of the valid period` + ); + return; + } + if (message.type === RegisterMessageType.RES) { + pendingResponses = pendingResponses - 1; + updatedRegistries = this.mergeErrors(updatedRegistries, worker, message.errors); + if (pendingResponses === 0 && timeOutHandler) { + onFinished(); + } + } + }; + const onTimeOut = () => { + // Stryker disable next-line all + this.logger.debug(`Timeout for update response from workers - ${pendingResponses} pending`); + onFinished(); + }; + const onFinished = () => { + // Stryker disable next-line all + this.logger.debug('Registry update from workers finished'); + cluster.off('message', onWorkerResponse); + if (timeOutHandler) { + clearTimeout(timeOutHandler); + timeOutHandler = undefined; + } + this.aggregator.updateWorkersErrors(updatedRegistries); + }; + timeOutHandler = setTimeout(onTimeOut, this.interval * 0.9); + cluster.on('message', onWorkerResponse); + for (const worker of Object.values(this.workers)) { + if (worker?.isConnected()) { + // Stryker disable next-line all + this.logger.debug( + `Sending an errors register update request to worker [${worker.process.pid}]` + ); + worker.send({ + type: RegisterMessageType.REQ, + requestId: this.requestId, + }); + } + } + }; + /** + * Merges the errors received from a worker process into the accumulated error records. + * Adds worker identification details to each error record for traceability. + * @param feed - feed to be merged with the errors from the worker + * @param worker - Worker that emit the errors + * @param workerErrors - errors from the worker + */ + private mergeErrors( + feed: ErrorRecord[], + worker: Worker, + workerErrors: ErrorRecord[] + ): ErrorRecord[] { + return feed.concat( + workerErrors.map(error => { + return { + ...error, + workerPid: worker.process.pid, + workerId: worker.id, + }; + }) + ); + } + /** + * Retrieves a dictionary of currently active worker processes. + * @returns A dictionary of Worker instances indexed by their cluster worker ID. + */ + private get workers(): NodeJS.Dict { + return cluster.workers ? cluster.workers : {}; + } +} diff --git a/packages/components/service-registry/src/observability/registries/errors/RegisterFacade.ts b/packages/components/service-registry/src/observability/registries/errors/RegisterFacade.ts index 33c8df61..3b8ed525 100644 --- a/packages/components/service-registry/src/observability/registries/errors/RegisterFacade.ts +++ b/packages/components/service-registry/src/observability/registries/errors/RegisterFacade.ts @@ -1,181 +1,182 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health, Layer } from '@mdf.js/core'; -import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; -import cluster from 'cluster'; -import EventEmitter from 'events'; -import express from 'express'; -import { Aggregator } from './Aggregator'; -import { MasterPort, Port, WorkerPort } from './Ports'; -import { Router } from './Router'; -import { ErrorRecord, HandleableError, REGISTER_SERVICE_NAME, RegistryOptions } from './types'; - -/** - * The RegisterFacade class provides a centralized solution for error monitoring across all - * components of an application. It acts as a facade over various underlying mechanisms to - * facilitate error aggregation, error information exposure through REST APIs, and error registry - * management. - * - * It integrates with: - * - Port: To handle inter-process communication for error information in clustered environments, - * distinguishing between master and worker processes. - * - Aggregator: To aggregate error information from different components of the application. - * - * This class also provides a REST API endpoint for accessing collected error information and - * supports operations for registering errors and clearing the error registry. - */ -export class RegisterFacade extends EventEmitter implements Layer.App.Service { - /** Debug logger for development and deep troubleshooting */ - private readonly logger: LoggerInstance; - /** Health aggregator */ - private readonly aggregator: Aggregator; - /** Health registry */ - private readonly port?: Port; - /** Health router */ - private readonly _router: Router; - /** - * Create an instance of register manager - * @param options - registry options - */ - constructor(private readonly options: RegistryOptions) { - super(); - this.logger = SetContext( - // Stryker disable next-line all - options.logger || new DebugLogger(`mdf:registry:errors:${this.name}`), - this.name, - this.componentId - ); - this.aggregator = new Aggregator(this.logger, this.options.maxSize, this.options.includeStack); - this.port = this.getPort(options, this.aggregator, this.logger); - this._router = new Router(this.aggregator); - // Stryker disable next-line all - this.logger.debug(`New error registry instance created`); - } - /** - * Determines and initializes the appropriate Port instance based on the operating context - * (master, worker, or standalone process) to manage error registry communication and updates. - * - * @param options - Registry and operational options. - * @param aggregator - The aggregator instance for error collection. - * @param logger - Logger instance for logging activities. - * @returns An instance of Port or undefined if running in a standalone process. - */ - private getPort( - options: RegistryOptions, - aggregator: Aggregator, - logger: LoggerInstance - ): Port | undefined { - if (typeof options.isCluster === 'boolean') { - return options.isCluster && cluster.isPrimary - ? new MasterPort(aggregator, logger, options.clusterUpdateInterval) - : new WorkerPort(aggregator, logger); - } - return undefined; // No port for standalone process - } - /** @returns The application name */ - public get name(): string { - return this.options.name; - } - /** @returns The application identifier */ - public get componentId(): string { - return this.options.instanceId; - } - /** @returns An Express router with access to registered errors */ - public get router(): express.Router { - return this._router.router; - } - /** @returns Links offered by this service */ - public get links(): { [link: string]: string } { - return { [`${REGISTER_SERVICE_NAME}`]: `/${REGISTER_SERVICE_NAME}` }; - } - /** @returns The health status of the component */ - public get status(): Health.Status { - return this.size > 0 ? Health.STATUS.WARN : Health.STATUS.PASS; - } - /** @returns Health checks for this service */ - public get checks(): Health.Checks { - return { - [`${this.name}:errors`]: [ - { - componentId: this.componentId, - processId: process.pid, - componentType: 'system', - observedValue: this.size, - observedUnit: 'errors', - status: this.status, - time: this.lastUpdate, - }, - ], - }; - } - /** - * Registers one or multiple components to be monitored. - * @param services - A single component or an array of component to be registered. - */ - public register(component: Layer.Observable | Layer.Observable[]): void { - this.aggregator.register(component); - } - /** - * Adds an error to the registry, converting it to a structured format. - * @param error - The error to register. - */ - public push(error: HandleableError): void { - this.aggregator.push(error); - } - /** Clear the error registry */ - public clear(): void { - this.aggregator.clear(); - this.port?.clear(); - } - /** @returns Returns a combined list of all the registered errors */ - public get errors(): ErrorRecord[] { - return this.aggregator.errors; - } - /** @returns The current number of registered errors */ - public get size(): number { - return this.aggregator.size; - } - /** @returns Last update date */ - public get lastUpdate(): string { - return this.aggregator.lastUpdate; - } - /** - * Starts the error registry service, including the communication port and error event listeners. - */ - public async start(): Promise { - this.aggregator.on('error', this.errorEventHandler); - this.port?.start(); - // Stryker disable next-line all - this.logger.debug('Error registry service started'); - } - /** - * Stops the error registry service, including halting communication and removing event listeners. - */ - public async stop(): Promise { - this.aggregator.off('error', this.errorEventHandler); - this.port?.stop(); - // Stryker disable next-line all - this.logger.debug('Error registry service stopped'); - } - /** Closes the error registry service, performing cleanup actions as necessary. */ - public async close(): Promise { - this.aggregator.close(); - await this.stop(); - // Stryker disable next-line all - this.logger.debug('Error registry service closed'); - } - /** - * Event handler for error event - * @param error - Error triggered by the component - */ - private readonly errorEventHandler = (error: ErrorRecord): void => { - if (this.listenerCount('error') > 0) { - this.emit('error', error); - } - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health, Layer } from '@mdf.js/core'; +import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; +import cluster from 'cluster'; +import EventEmitter from 'events'; +import express from 'express'; +import { Aggregator } from './Aggregator'; +import { MasterPort, Port, WorkerPort } from './Ports'; +import { Router } from './Router'; +import { ErrorRecord, HandleableError, REGISTER_SERVICE_NAME, RegistryOptions } from './types'; + +/** + * The RegisterFacade class provides a centralized solution for error monitoring across all + * components of an application. It acts as a facade over various underlying mechanisms to + * facilitate error aggregation, error information exposure through REST APIs, and error registry + * management. + * + * It integrates with: + * - Port: To handle inter-process communication for error information in clustered environments, + * distinguishing between master and worker processes. + * - Aggregator: To aggregate error information from different components of the application. + * + * This class also provides a REST API endpoint for accessing collected error information and + * supports operations for registering errors and clearing the error registry. + */ +export class RegisterFacade extends EventEmitter implements Layer.App.Service { + /** Debug logger for development and deep troubleshooting */ + private readonly logger: LoggerInstance; + /** Health aggregator */ + private readonly aggregator: Aggregator; + /** Health registry */ + private readonly port?: Port; + /** Health router */ + private readonly _router: Router; + /** + * Create an instance of register manager + * @param options - registry options + */ + constructor(private readonly options: RegistryOptions) { + super(); + this.logger = SetContext( + // Stryker disable next-line all + options.logger || new DebugLogger(`mdf:registry:errors:${this.name}`), + this.name, + this.componentId + ); + this.aggregator = new Aggregator(this.logger, this.options.maxSize, this.options.includeStack); + this.port = this.getPort(options, this.aggregator, this.logger); + this._router = new Router(this.aggregator); + // Stryker disable next-line all + this.logger.debug(`New error registry instance created`); + } + /** + * Determines and initializes the appropriate Port instance based on the operating context + * (master, worker, or standalone process) to manage error registry communication and updates. + * + * @param options - Registry and operational options. + * @param aggregator - The aggregator instance for error collection. + * @param logger - Logger instance for logging activities. + * @returns An instance of Port or undefined if running in a standalone process. + */ + private getPort( + options: RegistryOptions, + aggregator: Aggregator, + logger: LoggerInstance + ): Port | undefined { + if (typeof options.isCluster === 'boolean') { + return options.isCluster && cluster.isPrimary + ? new MasterPort(aggregator, logger, options.clusterUpdateInterval) + : new WorkerPort(aggregator, logger); + } + return undefined; // No port for standalone process + } + /** @returns The application name */ + public get name(): string { + return this.options.name; + } + /** @returns The application identifier */ + public get componentId(): string { + return this.options.instanceId; + } + /** @returns An Express router with access to registered errors */ + public get router(): express.Router { + return this._router.router; + } + /** @returns Links offered by this service */ + public get links(): { [link: string]: string } { + return { [`${REGISTER_SERVICE_NAME}`]: `/${REGISTER_SERVICE_NAME}` }; + } + /** @returns The health status of the component */ + public get status(): Health.Status { + return this.size > 0 ? Health.STATUS.WARN : Health.STATUS.PASS; + } + /** @returns Health checks for this service */ + public get checks(): Health.Checks { + return { + [`${this.name}:errors`]: [ + { + componentId: this.componentId, + processId: process.pid, + componentType: 'system', + observedValue: this.size, + observedUnit: 'errors', + status: this.status, + time: this.lastUpdate, + }, + ], + }; + } + /** + * Registers one or multiple components to be monitored. + * @param component - The component or components to register. + */ + public register(component: Layer.Observable | Layer.Observable[]): void { + this.aggregator.register(component); + } + /** + * Adds an error to the registry, converting it to a structured format. + * @param error - The error to register. + */ + public push(error: HandleableError): void { + this.aggregator.push(error); + } + /** Clear the error registry */ + public clear(): void { + this.aggregator.clear(); + this.port?.clear(); + } + /** @returns Returns a combined list of all the registered errors */ + public get errors(): ErrorRecord[] { + return this.aggregator.errors; + } + /** @returns The current number of registered errors */ + public get size(): number { + return this.aggregator.size; + } + /** @returns Last update date */ + public get lastUpdate(): string { + return this.aggregator.lastUpdate; + } + /** + * Starts the error registry service, including the communication port and error event listeners. + */ + public async start(): Promise { + this.aggregator.on('error', this.errorEventHandler); + this.port?.start(); + // Stryker disable next-line all + this.logger.debug('Error registry service started'); + } + /** + * Stops the error registry service, including halting communication and removing event listeners. + */ + public async stop(): Promise { + this.aggregator.off('error', this.errorEventHandler); + this.port?.stop(); + // Stryker disable next-line all + this.logger.debug('Error registry service stopped'); + } + /** Closes the error registry service, performing cleanup actions as necessary. */ + public async close(): Promise { + this.aggregator.close(); + await this.stop(); + // Stryker disable next-line all + this.logger.debug('Error registry service closed'); + } + /** + * Event handler for error event + * @param error - Error triggered by the component + */ + private readonly errorEventHandler = (error: ErrorRecord): void => { + if (this.listenerCount('error') > 0) { + this.emit('error', error); + } + }; +} + diff --git a/packages/components/service-registry/src/observability/registries/errors/index.ts b/packages/components/service-registry/src/observability/registries/errors/index.ts index 3aa80345..16d91d4f 100644 --- a/packages/components/service-registry/src/observability/registries/errors/index.ts +++ b/packages/components/service-registry/src/observability/registries/errors/index.ts @@ -1,14 +1,17 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { RegisterFacade as ErrorRegistry } from './RegisterFacade'; -export { - DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, - DEFAULT_CONFIG_REGISTER_INCLUDE_STACK, - DEFAULT_CONFIG_REGISTER_MAX_LIST_SIZE, - ErrorRecord, -} from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { RegisterFacade as ErrorRegistry } from './RegisterFacade'; +export { + DEFAULT_CONFIG_REGISTER_CLUSTER_UPDATE_INTERVAL, + DEFAULT_CONFIG_REGISTER_INCLUDE_STACK, + DEFAULT_CONFIG_REGISTER_MAX_LIST_SIZE, + ErrorRecord, + ExtendedCrashObject, + ExtendedMultiObject, +} from './types'; + diff --git a/packages/components/service-registry/src/observability/registries/errors/types/ErrorRecord.t.ts b/packages/components/service-registry/src/observability/registries/errors/types/ErrorRecord.t.ts index 63fa883d..e8ca0ce0 100644 --- a/packages/components/service-registry/src/observability/registries/errors/types/ErrorRecord.t.ts +++ b/packages/components/service-registry/src/observability/registries/errors/types/ErrorRecord.t.ts @@ -1,20 +1,23 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { CrashObject, MultiObject } from '@mdf.js/crash'; - -interface ExtendedCrashObject extends CrashObject { - workerId?: number; - workerPid?: number; - stack?: string; -} -interface ExtendedMultiObject extends MultiObject { - workerId?: number; - workerPid?: number; - stack?: string; -} - -export type ErrorRecord = ExtendedCrashObject | ExtendedMultiObject; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { CrashObject, MultiObject } from '@mdf.js/crash'; + +/** Extended Crash object including workerId, workerPid and stack */ +export interface ExtendedCrashObject extends CrashObject { + workerId?: number; + workerPid?: number; + stack?: string; +} +/** Extended Multi object including workerId, workerPid and stack */ +export interface ExtendedMultiObject extends MultiObject { + workerId?: number; + workerPid?: number; + stack?: string; +} + +/** Error record */ +export type ErrorRecord = ExtendedCrashObject | ExtendedMultiObject; diff --git a/packages/components/service-registry/src/observability/registries/health/Ports/MasterPort.ts b/packages/components/service-registry/src/observability/registries/health/Ports/MasterPort.ts index 3f05610b..c2c7739a 100644 --- a/packages/components/service-registry/src/observability/registries/health/Ports/MasterPort.ts +++ b/packages/components/service-registry/src/observability/registries/health/Ports/MasterPort.ts @@ -1,205 +1,206 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Health } from '@mdf.js/core'; -import { LoggerInstance } from '@mdf.js/logger'; -import cluster, { Worker } from 'cluster'; -import { Aggregator } from '../Aggregator'; -import { - DEFAULT_CONFIG_HEALTH_CLUSTER_UPDATE_INTERVAL, - HealthMessage, - HealthMessageType, - SYSTEM_WORKER, - SYSTEM_WORKER_HEALTH, - WORKER_CONNECTION_STATE, - WORKER_STATUS, -} from '../types'; -import { Port } from './Port'; - -/** - * The MasterPort class facilitates health monitoring within a master process of a clustered Node.js - * application. - * It periodically requests health checks from worker processes and aggregates their responses to - * maintain an updated view of the application's overall health status. - * - * Inherits from the Port class, leveraging shared functionality and adding specific mechanisms for - * handling health diagnostic requests and responses in a cluster master context. - */ -export class MasterPort extends Port { - /** Request sequence number */ - private requestId: number = 0; - /** Timeout interval handler for master polling */ - private timeInterval?: NodeJS.Timeout; - /** - * Create an instance of health manager in a master process - * @param aggregator - Aggregator instance for aggregating health checks from workers. - * @param logger - Logger instance for logging activities - * @param interval - interval in milliseconds between each health registry poll from workers. - */ - constructor( - private readonly aggregator: Aggregator, - logger: LoggerInstance, - private readonly interval: number = DEFAULT_CONFIG_HEALTH_CLUSTER_UPDATE_INTERVAL - ) { - super(logger); - // Stryker disable next-line all - this.logger.debug(`New master port instance created: ${JSON.stringify({ interval })}`); - } - /** - * Starts the process of periodically polling health registries from worker processes. - * Ensures that only one polling mechanism is active at any given time. - */ - public start(): void { - // Stryker disable next-line all - this.logger.debug('Starting health registry polling in master process'); - this.aggregator.updateWorkersChecks(this.initializeWorkersChecks()); - if (!this.timeInterval) { - this.onSendRequest(); - this.timeInterval = setInterval(this.onSendRequest, this.interval); - } - } - /** - * Stops the polling of health registries from worker processes and clears the polling interval. - */ - public stop(): void { - // Stryker disable next-line all - this.logger.debug('Stopping health registry polling in master process'); - if (this.timeInterval) { - clearInterval(this.timeInterval); - this.timeInterval = undefined; - } - } - /** - * Sends a request to all worker processes to send their current health registries. - * Handles responses, timeouts, and updates the aggregator with aggregated health checks from - * workers. - */ - private readonly onSendRequest = (): void => { - this.requestId = this.requestId + 1; - if (!Number.isFinite(this.requestId)) { - this.requestId = 0; - } - let pendingResponses = Object.keys(this.workers).length; - let timeOutHandler: NodeJS.Timeout | undefined; - let updatedChecks: Health.Checks = this.initializeWorkersChecks(); - const onWorkerResponse = (worker: Worker, message: HealthMessage) => { - if (message.requestId !== this.requestId) { - // Stryker disable next-line all - this.logger.debug( - `Health response from worker [${worker.process.pid}] out of the valid period` - ); - return; - } - if (message.type === HealthMessageType.RES) { - pendingResponses = pendingResponses - 1; - updatedChecks = this.mergeChecks(updatedChecks, worker, message.checks); - if (pendingResponses === 0 && timeOutHandler) { - onFinished(); - } - } - }; - const onTimeOut = () => { - // Stryker disable next-line all - this.logger.debug( - `Timeout waiting for health response from workers - ${pendingResponses} pending` - ); - onFinished(); - }; - const onFinished = () => { - // Stryker disable next-line all - this.logger.debug('Health update finished'); - cluster.off('message', onWorkerResponse); - if (timeOutHandler) { - clearTimeout(timeOutHandler); - timeOutHandler = undefined; - } - this.aggregator.updateWorkersChecks(updatedChecks); - }; - timeOutHandler = setTimeout(onTimeOut, this.interval * 0.9); - cluster.on('message', onWorkerResponse); - for (const worker of Object.values(this.workers)) { - if (worker && worker.isConnected()) { - this.logger.debug(`Sending an health update request to worker [${worker.process.pid}]`); - worker.send({ - type: HealthMessageType.REQ, - requestId: this.requestId, - }); - } - } - }; - /** - * Initializes health checks for all workers, setting up baseline checks for connection status - * and default health status as 'outdated' until updated by worker responses. - * @returns Initial checks for all workers. - */ - private initializeWorkersChecks(): Health.Checks { - const checks: Health.Checks = { [SYSTEM_WORKER]: [], [SYSTEM_WORKER_HEALTH]: [] }; - for (const worker of Object.values(this.workers)) { - if (worker) { - checks[SYSTEM_WORKER].push({ - componentId: `${worker.process.pid}`, - componentType: 'process', - status: worker.isConnected() ? Health.STATUS.PASS : Health.STATUS.FAIL, - observedValue: worker.isConnected() - ? WORKER_CONNECTION_STATE.CONNECTED - : WORKER_CONNECTION_STATE.DISCONNECTED, - observedUnit: 'status', - workerId: worker.id, - workerPid: worker.process.pid, - time: new Date().toISOString(), - }); - checks[SYSTEM_WORKER_HEALTH].push({ - componentId: `${worker.process.pid}`, - componentType: 'process', - status: Health.STATUS.FAIL, - observedValue: WORKER_STATUS.OUTDATED, - observedUnit: 'status', - workerId: worker.id, - workerPid: worker.process.pid, - time: new Date().toISOString(), - }); - } - } - return checks; - } - /** - * Merge the checks from a worker, including in them the PID and worker identifier, with passed - * feed passed as argument - * @param feed - feed where to merge the checks - * @param worker - Worker that emit the checks - * @param checks - Checks to be merged - * @returns The feed with the merged checks - */ - private mergeChecks(feed: Health.Checks, worker: Worker, checks: Health.Checks): Health.Checks { - for (const [key, value] of Object.entries(checks)) { - const entry = key as Health.CheckEntry; - const entryChecks = value.map(check => ({ - ...check, - workerId: worker.id, - workerPid: worker.process.pid, - })); - feed[entry] = feed[entry] ? feed[entry].concat(entryChecks) : entryChecks; - } - if (feed[SYSTEM_WORKER_HEALTH]) { - const result = feed[SYSTEM_WORKER_HEALTH].find( - workerEntry => workerEntry['workerId'] === worker.id - ); - if (result) { - result['observedValue'] = WORKER_STATUS.UPDATED; - result['status'] = Health.STATUS.PASS; - } - } - return feed; - } - /** - * Retrieves a dictionary of currently active worker processes. - * @returns A dictionary of Worker instances indexed by their cluster worker ID. - */ - private get workers(): NodeJS.Dict { - return cluster.workers ? cluster.workers : {}; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Health } from '@mdf.js/core'; +import { LoggerInstance } from '@mdf.js/logger'; +import cluster, { Worker } from 'cluster'; +import { Aggregator } from '../Aggregator'; +import { + DEFAULT_CONFIG_HEALTH_CLUSTER_UPDATE_INTERVAL, + HealthMessage, + HealthMessageType, + SYSTEM_WORKER, + SYSTEM_WORKER_HEALTH, + WORKER_CONNECTION_STATE, + WORKER_STATUS, +} from '../types'; +import { Port } from './Port'; + +/** + * The MasterPort class facilitates health monitoring within a master process of a clustered Node.js + * application. + * It periodically requests health checks from worker processes and aggregates their responses to + * maintain an updated view of the application's overall health status. + * + * Inherits from the Port class, leveraging shared functionality and adding specific mechanisms for + * handling health diagnostic requests and responses in a cluster master context. + */ +export class MasterPort extends Port { + /** Request sequence number */ + private requestId: number = 0; + /** Timeout interval handler for master polling */ + private timeInterval?: NodeJS.Timeout; + /** + * Create an instance of health manager in a master process + * @param aggregator - Aggregator instance for aggregating health checks from workers. + * @param logger - Logger instance for logging activities + * @param interval - interval in milliseconds between each health registry poll from workers. + */ + constructor( + private readonly aggregator: Aggregator, + logger: LoggerInstance, + private readonly interval: number = DEFAULT_CONFIG_HEALTH_CLUSTER_UPDATE_INTERVAL + ) { + super(logger); + // Stryker disable next-line all + this.logger.debug(`New master port instance created: ${JSON.stringify({ interval })}`); + } + /** + * Starts the process of periodically polling health registries from worker processes. + * Ensures that only one polling mechanism is active at any given time. + */ + public start(): void { + // Stryker disable next-line all + this.logger.debug('Starting health registry polling in master process'); + this.aggregator.updateWorkersChecks(this.initializeWorkersChecks()); + if (!this.timeInterval) { + this.onSendRequest(); + this.timeInterval = setInterval(this.onSendRequest, this.interval); + } + } + /** + * Stops the polling of health registries from worker processes and clears the polling interval. + */ + public stop(): void { + // Stryker disable next-line all + this.logger.debug('Stopping health registry polling in master process'); + if (this.timeInterval) { + clearInterval(this.timeInterval); + this.timeInterval = undefined; + } + } + /** + * Sends a request to all worker processes to send their current health registries. + * Handles responses, timeouts, and updates the aggregator with aggregated health checks from + * workers. + */ + private readonly onSendRequest = (): void => { + this.requestId = this.requestId + 1; + if (!Number.isFinite(this.requestId)) { + this.requestId = 0; + } + let pendingResponses = Object.keys(this.workers).length; + let timeOutHandler: NodeJS.Timeout | undefined; + let updatedChecks: Health.Checks = this.initializeWorkersChecks(); + const onWorkerResponse = (worker: Worker, message: HealthMessage) => { + if (message.requestId !== this.requestId) { + // Stryker disable next-line all + this.logger.debug( + `Health response from worker [${worker.process.pid}] out of the valid period` + ); + return; + } + if (message.type === HealthMessageType.RES) { + pendingResponses = pendingResponses - 1; + updatedChecks = this.mergeChecks(updatedChecks, worker, message.checks); + if (pendingResponses === 0 && timeOutHandler) { + onFinished(); + } + } + }; + const onTimeOut = () => { + // Stryker disable next-line all + this.logger.debug( + `Timeout waiting for health response from workers - ${pendingResponses} pending` + ); + onFinished(); + }; + const onFinished = () => { + // Stryker disable next-line all + this.logger.debug('Health update finished'); + cluster.off('message', onWorkerResponse); + if (timeOutHandler) { + clearTimeout(timeOutHandler); + timeOutHandler = undefined; + } + this.aggregator.updateWorkersChecks(updatedChecks); + }; + timeOutHandler = setTimeout(onTimeOut, this.interval * 0.9); + cluster.on('message', onWorkerResponse); + for (const worker of Object.values(this.workers)) { + if (worker?.isConnected()) { + this.logger.debug(`Sending an health update request to worker [${worker.process.pid}]`); + worker.send({ + type: HealthMessageType.REQ, + requestId: this.requestId, + }); + } + } + }; + /** + * Initializes health checks for all workers, setting up baseline checks for connection status + * and default health status as 'outdated' until updated by worker responses. + * @returns Initial checks for all workers. + */ + private initializeWorkersChecks(): Health.Checks { + const checks: Health.Checks = { [SYSTEM_WORKER]: [], [SYSTEM_WORKER_HEALTH]: [] }; + for (const worker of Object.values(this.workers)) { + if (worker) { + checks[SYSTEM_WORKER].push({ + componentId: `${worker.process.pid}`, + componentType: 'process', + status: worker.isConnected() ? Health.STATUS.PASS : Health.STATUS.FAIL, + observedValue: worker.isConnected() + ? WORKER_CONNECTION_STATE.CONNECTED + : WORKER_CONNECTION_STATE.DISCONNECTED, + observedUnit: 'status', + workerId: worker.id, + workerPid: worker.process.pid, + time: new Date().toISOString(), + }); + checks[SYSTEM_WORKER_HEALTH].push({ + componentId: `${worker.process.pid}`, + componentType: 'process', + status: Health.STATUS.FAIL, + observedValue: WORKER_STATUS.OUTDATED, + observedUnit: 'status', + workerId: worker.id, + workerPid: worker.process.pid, + time: new Date().toISOString(), + }); + } + } + return checks; + } + /** + * Merge the checks from a worker, including in them the PID and worker identifier, with passed + * feed passed as argument + * @param feed - feed where to merge the checks + * @param worker - Worker that emit the checks + * @param checks - Checks to be merged + * @returns The feed with the merged checks + */ + private mergeChecks(feed: Health.Checks, worker: Worker, checks: Health.Checks): Health.Checks { + for (const [key, value] of Object.entries(checks)) { + const entry = key as Health.CheckEntry; + const entryChecks = value.map(check => ({ + ...check, + workerId: worker.id, + workerPid: worker.process.pid, + })); + feed[entry] = feed[entry] ? feed[entry].concat(entryChecks) : entryChecks; + } + if (feed[SYSTEM_WORKER_HEALTH]) { + const result = feed[SYSTEM_WORKER_HEALTH].find( + workerEntry => workerEntry['workerId'] === worker.id + ); + if (result) { + result['observedValue'] = WORKER_STATUS.UPDATED; + result['status'] = Health.STATUS.PASS; + } + } + return feed; + } + /** + * Retrieves a dictionary of currently active worker processes. + * @returns A dictionary of Worker instances indexed by their cluster worker ID. + */ + private get workers(): NodeJS.Dict { + return cluster.workers ? cluster.workers : {}; + } +} + diff --git a/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.test.ts b/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.test.ts index 06efb1e9..ef23fd60 100644 --- a/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.test.ts +++ b/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.test.ts @@ -1,217 +1,218 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -// ************************************************************************************************ -// #region Component imports -import { Boom, BoomHelpers, Crash, Multi } from '@mdf.js/crash'; -import { DebugLogger } from '@mdf.js/logger'; -import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; -import request from 'supertest'; -import { v4 } from 'uuid'; -import { Aggregator } from '../Aggregator'; -import { Router } from './metrics.router'; -// #endregion -// ************************************************************************************************* -// #region Own express app for testing, including the mandatory middleware -const app = express(); -const aggregator = new Aggregator(new DebugLogger(`test`)); -const metricsRouteAppCluster = new Router(aggregator, true, '/metricsCluster'); -const metricsRoute = new Router(aggregator); -app.use(metricsRoute.router); -app.use(metricsRouteAppCluster.router); -const errorHandler: ErrorRequestHandler = ( - error: Error | Crash | Multi, - request: Request, - response: Response, - next: NextFunction -) => { - const requestId: string = request.uuid || (request.headers['x-request-id'] as string) || v4(); - let boomError: Boom; - if (error instanceof Boom) { - boomError = error; - } else if ('isMulti' in error) { - boomError = BoomHelpers.badRequest(error.message, requestId, { - source: { - pointer: request.path, - parameter: { body: request.body, query: request.query }, - }, - cause: error, - name: error.name, - info: { ...error.trace() }, - }); - } else { - boomError = BoomHelpers.internalServerError(`${error.name}: ${error.message}`, requestId); - } - response.status(boomError.status).json(boomError); -}; -app.use(errorHandler); - -// #endregion -// ************************************************************************************************* -// #region Our tests -describe('#Component #metrics', () => { - describe('#Happy path', () => { - it(`Should response 200 and a string when a GET request is performed over /metrics allowing any content-type in regular mode`, done => { - request(app) - .get('/metrics') - .set('Content-Type', 'text/plain') - .set('Accept', '*/*') - .expect('Content-Type', /text\/plain/) - .expect(200) - .then(response => { - expect(typeof response.text).toEqual('string'); - done(); - }) - .catch(error => { - fail(error.message); - }); - }, 300); - it(`Should response 200 and a JSON body when a GET request is performed over /metrics allowing any content-type but with json flag in query in regular mode`, done => { - request(app) - .get('/metrics?json=true') - .set('Content-Type', 'text/plain') - .set('Accept', '*/*') - .expect('Content-Type', /json/) - .expect(200) - .then(response => { - expect(typeof response.body).toEqual('object'); - done(); - }) - .catch(error => { - fail(error.message); - }); - }, 300); - it(`Should response 200 and a string when a GET request is performed over /metrics allowing only text/plain content type in regular mode`, done => { - request(app) - .get('/metrics') - .set('Content-Type', 'text/plain') - .set('Accept', 'text/plain') - .expect('Content-Type', /text\/plain/) - .expect(200) - .then(response => { - expect(typeof response.text).toEqual('string'); - done(); - }) - .catch(error => { - fail(error.message); - }); - }, 300); - it(`Should response 200 and a JSON body when a GET request is performed over /metrics allowing only json/application content/type in regular mode`, done => { - request(app) - .get('/metrics') - .set('Content-Type', 'text/plain') - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200) - .then(response => { - expect(typeof response.body).toEqual('object'); - done(); - }) - .catch(error => { - fail(error.message); - }); - }, 300); - it(`Should response 200 and a string when a GET request is performed over /metrics allowing only text/plain content type in cluster mode`, done => { - request(app) - .get('/metricsCluster') - .set('Content-Type', 'text/plain') - .set('Accept', 'text/plain') - .expect('Content-Type', /text\/plain/) - .expect(200) - .then(response => { - expect(typeof response.text).toEqual('string'); - done(); - }) - .catch(error => { - fail(error.message); - }); - }, 300); - }); - describe('#Sad path', () => { - it(`Should response 400 as bad request error when a GET request is performed and the query has not the correct mode`, done => { - request(app) - .get('/metrics?json=3') - .set('Content-Type', 'text/plain') - .set('Accept', 'text/plain') - .expect('Content-Type', /json/) - .expect(400) - .then(response => { - expect(response.body.status).toEqual(400); - expect(response.body.code).toEqual('ValidationError'); - expect(response.body.title).toEqual('Bad Request'); - expect(response.body.detail).toEqual('Errors during the schema validation process'); - expect(response.body.source).toEqual({ - pointer: '/metrics', - parameter: { - query: { - json: '3', - }, - }, - }); - expect(response.body.meta).toEqual({ - '0': 'ValidationError: Should be a boolean - Path: [/json] - Value: [3]', - }); - done(); - }) - .catch(error => { - throw error; - }); - }, 300); - it(`Should response 406 as not acceptable request error when a GET request is performed and text/plain or application/json are not allowed in regular mode`, done => { - request(app) - .get('/metrics') - .set('Content-Type', 'text/plain') - .set('Accept', 'other/other') - .expect('Content-Type', /json/) - .expect(406) - .then(response => { - expect(response.body.status).toEqual(406); - expect(response.body.code).toEqual('HTTP'); - expect(response.body.title).toEqual('Not Acceptable'); - expect(response.body.detail).toEqual( - 'Not valid formats for metrics endpoint are aceptable by the client' - ); - expect(response.body.source).toEqual({ - pointer: '/metrics', - parameter: { - query: {}, - }, - }); - done(); - }) - .catch(error => { - throw error; - }); - }, 300); - it(`Should response 406 as not acceptable request error when a GET request is performed and text/plain is not allowed in cluster mode`, done => { - request(app) - .get('/metricsCluster') - .set('Content-Type', 'text/plain') - .set('Accept', 'other/other') - .expect('Content-Type', /json/) - .expect(406) - .then(response => { - expect(response.body.status).toEqual(406); - expect(response.body.code).toEqual('HTTP'); - expect(response.body.title).toEqual('Not Acceptable'); - expect(response.body.detail).toEqual( - 'Not valid formats for metrics endpoint are aceptable by the client' - ); - expect(response.body.source).toEqual({ - pointer: '/metricsCluster', - parameter: { - query: {}, - }, - }); - done(); - }) - .catch(error => { - throw error; - }); - }, 300); - }); -}); -// #endregion +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +// ************************************************************************************************ +// #region Component imports +import { Boom, BoomHelpers, Crash, Multi } from '@mdf.js/crash'; +import { DebugLogger } from '@mdf.js/logger'; +import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; +import request from 'supertest'; +import { v4 } from 'uuid'; +import { Aggregator } from '../Aggregator'; +import { Router } from './metrics.router'; +// #endregion +// ************************************************************************************************* +// #region Own express app for testing, including the mandatory middleware +const app = express(); +const aggregator = new Aggregator(new DebugLogger(`test`)); +const metricsRouteAppCluster = new Router(aggregator, true, '/metricsCluster'); +const metricsRoute = new Router(aggregator); +app.use(metricsRoute.router); +app.use(metricsRouteAppCluster.router); +const errorHandler: ErrorRequestHandler = ( + error: Error | Crash | Multi, + request: Request, + response: Response, + next: NextFunction +) => { + const requestId: string = request.uuid || (request.headers['x-request-id'] as string) || v4(); + let boomError: Boom; + if (error instanceof Boom) { + boomError = error; + } else if ('isMulti' in error) { + boomError = BoomHelpers.badRequest(error.message, requestId, { + source: { + pointer: request.path, + parameter: { body: request.body, query: request.query }, + }, + cause: error, + name: error.name, + info: { ...error.trace() }, + }); + } else { + boomError = BoomHelpers.internalServerError(`${error.name}: ${error.message}`, requestId); + } + response.status(boomError.status).json(boomError); +}; +app.use(errorHandler); + +// #endregion +// ************************************************************************************************* +// #region Our tests +describe('#Component #metrics', () => { + describe('#Happy path', () => { + it(`Should response 200 and a string when a GET request is performed over /metrics allowing any content-type in regular mode`, done => { + request(app) + .get('/metrics') + .set('Content-Type', 'text/plain') + .set('Accept', '*/*') + .expect('Content-Type', /text\/plain/) + .expect(200) + .then(response => { + expect(typeof response.text).toEqual('string'); + done(); + }) + .catch(error => { + fail(error.message); + }); + }, 300); + it(`Should response 200 and a JSON body when a GET request is performed over /metrics allowing any content-type but with json flag in query in regular mode`, done => { + request(app) + .get('/metrics?json=true') + .set('Content-Type', 'text/plain') + .set('Accept', '*/*') + .expect('Content-Type', /json/) + .expect(200) + .then(response => { + expect(typeof response.body).toEqual('object'); + done(); + }) + .catch(error => { + fail(error.message); + }); + }, 300); + it(`Should response 200 and a string when a GET request is performed over /metrics allowing only text/plain content type in regular mode`, done => { + request(app) + .get('/metrics') + .set('Content-Type', 'text/plain') + .set('Accept', 'text/plain') + .expect('Content-Type', /text\/plain/) + .expect(200) + .then(response => { + expect(typeof response.text).toEqual('string'); + done(); + }) + .catch(error => { + fail(error.message); + }); + }, 300); + it(`Should response 200 and a JSON body when a GET request is performed over /metrics allowing only json/application content/type in regular mode`, done => { + request(app) + .get('/metrics') + .set('Content-Type', 'text/plain') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .then(response => { + expect(typeof response.body).toEqual('object'); + done(); + }) + .catch(error => { + fail(error.message); + }); + }, 300); + it(`Should response 200 and a string when a GET request is performed over /metrics allowing only text/plain content type in cluster mode`, done => { + request(app) + .get('/metricsCluster') + .set('Content-Type', 'text/plain') + .set('Accept', 'text/plain') + .expect('Content-Type', /text\/plain/) + .expect(200) + .then(response => { + expect(typeof response.text).toEqual('string'); + done(); + }) + .catch(error => { + fail(error.message); + }); + }, 300); + }); + describe('#Sad path', () => { + it(`Should response 400 as bad request error when a GET request is performed and the query has not the correct mode`, done => { + request(app) + .get('/metrics?json=3') + .set('Content-Type', 'text/plain') + .set('Accept', 'text/plain') + .expect('Content-Type', /json/) + .expect(400) + .then(response => { + expect(response.body.status).toEqual(400); + expect(response.body.code).toEqual('ValidationError'); + expect(response.body.title).toEqual('Bad Request'); + expect(response.body.detail).toEqual('Errors during the schema validation process'); + expect(response.body.source).toEqual({ + pointer: '/metrics', + parameter: { + query: { + json: '3', + }, + }, + }); + expect(response.body.meta).toEqual({ + '0': 'ValidationError: Should be a boolean - Path: [/json] - Value: ["3"]', + }); + done(); + }) + .catch(error => { + throw error; + }); + }, 300); + it(`Should response 406 as not acceptable request error when a GET request is performed and text/plain or application/json are not allowed in regular mode`, done => { + request(app) + .get('/metrics') + .set('Content-Type', 'text/plain') + .set('Accept', 'other/other') + .expect('Content-Type', /json/) + .expect(406) + .then(response => { + expect(response.body.status).toEqual(406); + expect(response.body.code).toEqual('HTTP'); + expect(response.body.title).toEqual('Not Acceptable'); + expect(response.body.detail).toEqual( + 'Not valid formats for metrics endpoint are aceptable by the client' + ); + expect(response.body.source).toEqual({ + pointer: '/metrics', + parameter: { + query: {}, + }, + }); + done(); + }) + .catch(error => { + throw error; + }); + }, 300); + it(`Should response 406 as not acceptable request error when a GET request is performed and text/plain is not allowed in cluster mode`, done => { + request(app) + .get('/metricsCluster') + .set('Content-Type', 'text/plain') + .set('Accept', 'other/other') + .expect('Content-Type', /json/) + .expect(406) + .then(response => { + expect(response.body.status).toEqual(406); + expect(response.body.code).toEqual('HTTP'); + expect(response.body.title).toEqual('Not Acceptable'); + expect(response.body.detail).toEqual( + 'Not valid formats for metrics endpoint are aceptable by the client' + ); + expect(response.body.source).toEqual({ + pointer: '/metricsCluster', + parameter: { + query: {}, + }, + }); + done(); + }) + .catch(error => { + throw error; + }); + }, 300); + }); +}); +// #endregion + diff --git a/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.validator.ts b/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.validator.ts index fb2c6562..1ce1285d 100644 --- a/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.validator.ts +++ b/packages/components/service-registry/src/observability/registries/metrics/Router/metrics.validator.ts @@ -1,37 +1,38 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Crash, Multi } from '@mdf.js/crash'; -import { coerce } from '@mdf.js/utils'; -import { NextFunction, Request, Response } from 'express'; - -/** Validator class */ -export class Validator { - /** - * Return all the actual metrics of this artifact - * @param request - HTTP request express object - * @param response - HTTP response express object - * @param next - Next express middleware function - */ - public metrics(request: Request, response: Response, next: NextFunction): void { - if (request.query['json'] && typeof coerce(request.query['json'] as string) !== 'boolean') { - const validationError = new Multi( - `Errors during the schema validation process`, - request.uuid, - { name: 'ValidationError' } - ); - const jsonFormatError = new Crash( - `Should be a boolean - Path: [/json] - Value: [${request.query['json']}]`, - request.uuid, - { name: 'ValidationError' } - ); - validationError.push(jsonFormatError); - next(validationError); - } else { - next(); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Crash, Multi } from '@mdf.js/crash'; +import { coerce } from '@mdf.js/utils'; +import { NextFunction, Request, Response } from 'express'; + +/** Validator class */ +export class Validator { + /** + * Return all the actual metrics of this artifact + * @param request - HTTP request express object + * @param response - HTTP response express object + * @param next - Next express middleware function + */ + public metrics(request: Request, response: Response, next: NextFunction): void { + if (request.query['json'] && typeof coerce(request.query['json'] as string) !== 'boolean') { + const validationError = new Multi( + `Errors during the schema validation process`, + request.uuid, + { name: 'ValidationError' } + ); + const jsonFormatError = new Crash( + `Should be a boolean - Path: [/json] - Value: [${JSON.stringify(request.query['json'])}]`, + request.uuid, + { name: 'ValidationError' } + ); + validationError.push(jsonFormatError); + next(validationError); + } else { + next(); + } + } +} + diff --git a/packages/components/service-registry/src/settings/Router/config.test.ts b/packages/components/service-registry/src/settings/Router/config.test.ts index 41819974..2fd947da 100644 --- a/packages/components/service-registry/src/settings/Router/config.test.ts +++ b/packages/components/service-registry/src/settings/Router/config.test.ts @@ -1,178 +1,181 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -// ************************************************************************************************ -// #region Component imports -import express from 'express'; -import request from 'supertest'; -import { SettingsManager } from '..'; -import { CONFIG_SERVICE_NAME } from '../types'; -import { Router } from './config.router'; -// #endregion -// ************************************************************************************************* -// #region Own express app for testing, including the mandatory middleware -const app = express(); -const manager = new SettingsManager( - { - loadPackage: true, - loadReadme: true, - }, - { - configLoaderOptions: { - configFiles: [`${__dirname}/../../__mocks__/*.*`], - presetFiles: [`${__dirname}/../../__mocks__/presets/*.*`], - schemaFiles: [`${__dirname}/../../__mocks__/schemas/*.*`], - schema: 'final', - preset: 'preset1', - envPrefix: 'MY_PREFIX_A_', - }, - }, - {} -); -const configRouter = new Router(manager); -app.use(configRouter.router); - -// #endregion -// ************************************************************************************************* -// #region Our tests -describe('#Component #service-setup', () => { - describe('#Happy path', () => { - it(`Should response 200 and json object with all the presets when a GET request is performed over /${CONFIG_SERVICE_NAME}/presets`, done => { - request(app) - .get(`/${CONFIG_SERVICE_NAME}/presets`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200) - .then(response => { - expect(response.body).toEqual({ - preset1: { - config: { - test: 2, - }, - otherConfig: { - otherTest: 'a', - }, - }, - preset2: { - config: { - test: 4, - }, - otherConfig: { - otherTest: 'b', - }, - }, - preset3: { - config: { - test: 5, - }, - otherConfig: { - otherTest: 'j', - }, - }, - }); - done(); - }) - .catch(error => { - done(error); - }); - }, 300); - it(`Should response 200 and json object with all the presets when a GET request is performed over /${CONFIG_SERVICE_NAME}/config`, done => { - request(app) - .get(`/${CONFIG_SERVICE_NAME}/config`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200) - .then(response => { - expect(response.body).toEqual({ - metadata: { - name: '@mdf.js/service-registry', - description: 'MMS - API - Service Registry', - instanceId: expect.any(String), - release: '0.0.1', - version: '0', - tags: ['NodeJS', 'MMS', 'API', 'APP'], - }, - loggerOptions: { - console: { - enabled: true, - level: 'info', - }, - file: { - enabled: false, - level: 'info', - }, - }, - observabilityOptions: { - clusterUpdateInterval: 10000, - host: 'localhost', - includeStack: false, - isCluster: false, - maxSize: 100, - primaryPort: 9081, - }, - retryOptions: { - attempts: 3, - maxWaitTime: 10000, - timeout: 5000, - waitTime: 1000, - }, - config: { - test: 2, - }, - configLoaderOptions: { - configFiles: [expect.stringContaining('/../../__mocks__/*.*')], - presetFiles: [expect.stringContaining('/../../__mocks__/presets/*.*')], - schemaFiles: [expect.stringContaining('/../../__mocks__/schemas/*.*')], - schema: 'final', - preset: 'preset1', - envPrefix: 'MY_PREFIX_A_', - }, - otherConfig: { - otherTest: 'a', - }, - }); - done(); - }) - .catch(error => { - done(error); - }); - }, 300); - it(`Should response 200 and html text when a GET request is performed over /${CONFIG_SERVICE_NAME}/readme`, done => { - request(app) - .get(`/${CONFIG_SERVICE_NAME}/readme`) - .set('Content-Type', 'html/text') - .set('Accept', 'html/text') - .expect('Content-Type', /text/) - .expect(200) - .then(response => { - expect(response.body).toEqual({}); - done(); - }) - .catch(error => { - done(error); - }); - }, 300); - }); - describe('#Sad path', () => { - it(`Should response 404 when a GET request is performed over /${CONFIG_SERVICE_NAME}/unknown`, done => { - request(app) - .get(`/${CONFIG_SERVICE_NAME}/unknown`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(400) - .then(response => { - expect(response.body).toEqual({}); - done(); - }) - .catch(error => { - done(error); - }); - }, 300); - }); -}); -// #endregion +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +// ************************************************************************************************ +// #region Component imports +import express from 'express'; +import request from 'supertest'; +import { SettingsManager } from '..'; +import { CONFIG_SERVICE_NAME } from '../types'; +import { Router } from './config.router'; +// #endregion +// ************************************************************************************************* +// #region Own express app for testing, including the mandatory middleware +const app = express(); +const manager = new SettingsManager( + { + loadPackage: true, + loadReadme: true, + }, + { + configLoaderOptions: { + configFiles: [`${__dirname}/../../__mocks__/*.*`], + presetFiles: [`${__dirname}/../../__mocks__/presets/*.*`], + schemaFiles: [`${__dirname}/../../__mocks__/schemas/*.*`], + schema: 'final', + preset: 'preset1', + envPrefix: 'MY_PREFIX_A_', + }, + }, + {} +); +const configRouter = new Router(manager); +app.use(configRouter.router); + +// #endregion +// ************************************************************************************************* +// #region Our tests +describe('#Component #service-setup', () => { + describe('#Happy path', () => { + it(`Should response 200 and json object with all the presets when a GET request is performed over /${CONFIG_SERVICE_NAME}/presets`, done => { + request(app) + .get(`/${CONFIG_SERVICE_NAME}/presets`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .then(response => { + expect(response.body).toEqual({ + preset1: { + config: { + test: 2, + }, + otherConfig: { + otherTest: 'a', + }, + }, + preset2: { + config: { + test: 4, + }, + otherConfig: { + otherTest: 'b', + }, + }, + preset3: { + config: { + test: 5, + }, + otherConfig: { + otherTest: 'j', + }, + }, + }); + done(); + }) + .catch(error => { + done(error); + }); + }, 300); + it(`Should response 200 and json object with all the presets when a GET request is performed over /${CONFIG_SERVICE_NAME}/config`, done => { + request(app) + .get(`/${CONFIG_SERVICE_NAME}/config`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .then(response => { + expect(response.body).toEqual({ + metadata: { + name: '@mdf.js/service-registry', + description: 'MMS - API - Service Registry', + instanceId: expect.any(String), + release: '0.0.1', + version: '0', + tags: ['NodeJS', 'MMS', 'API', 'APP'], + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + }, + loggerOptions: { + console: { + enabled: true, + level: 'info', + }, + file: { + enabled: false, + level: 'info', + }, + }, + observabilityOptions: { + clusterUpdateInterval: 10000, + host: 'localhost', + includeStack: false, + isCluster: false, + maxSize: 100, + primaryPort: 9081, + }, + retryOptions: { + attempts: 3, + maxWaitTime: 10000, + timeout: 5000, + waitTime: 1000, + }, + config: { + test: 2, + }, + configLoaderOptions: { + configFiles: [expect.stringContaining('/../../__mocks__/*.*')], + presetFiles: [expect.stringContaining('/../../__mocks__/presets/*.*')], + schemaFiles: [expect.stringContaining('/../../__mocks__/schemas/*.*')], + schema: 'final', + preset: 'preset1', + envPrefix: 'MY_PREFIX_A_', + }, + otherConfig: { + otherTest: 'a', + }, + }); + done(); + }) + .catch(error => { + done(error); + }); + }, 300); + it(`Should response 200 and html text when a GET request is performed over /${CONFIG_SERVICE_NAME}/readme`, done => { + request(app) + .get(`/${CONFIG_SERVICE_NAME}/readme`) + .set('Content-Type', 'html/text') + .set('Accept', 'html/text') + .expect('Content-Type', /text/) + .expect(200) + .then(response => { + expect(response.body).toEqual({}); + done(); + }) + .catch(error => { + done(error); + }); + }, 300); + }); + describe('#Sad path', () => { + it(`Should response 404 when a GET request is performed over /${CONFIG_SERVICE_NAME}/unknown`, done => { + request(app) + .get(`/${CONFIG_SERVICE_NAME}/unknown`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(400) + .then(response => { + expect(response.body).toEqual({}); + done(); + }) + .catch(error => { + done(error); + }); + }, 300); + }); +}); +// #endregion + diff --git a/packages/components/service-registry/src/settings/SettingsManager.test.ts b/packages/components/service-registry/src/settings/SettingsManager.test.ts index 7b085a86..4ecb6b69 100644 --- a/packages/components/service-registry/src/settings/SettingsManager.test.ts +++ b/packages/components/service-registry/src/settings/SettingsManager.test.ts @@ -1,175 +1,181 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash, Multi } from '@mdf.js/crash'; -import fs from 'fs'; -import { SettingsManager } from '.'; -describe('#SettingsManager', () => { - describe('#Happy path', () => { - beforeEach(() => {}); - it(`Should create a valid instance of SettingsManager with default values`, async () => { - const manager = new SettingsManager(); - expect(manager).toBeInstanceOf(SettingsManager); - expect(manager.name).toEqual('settings'); - expect(manager.componentId).toBeDefined(); - expect(manager.status).toEqual('warn'); - const checks = manager.checks; - expect(checks).toEqual({ - 'mdf-app:settings': [ - { - status: 'warn', - componentId: manager.componentId, - componentType: 'setup service', - observedValue: 'stopped', - time: checks['mdf-app:settings'][0].time, - output: undefined, - scope: 'ServiceRegistry', - observedUnit: 'status', - }, - { - status: 'warn', - componentId: manager.componentId, - componentType: 'setup service', - observedValue: 'stopped', - time: checks['mdf-app:settings'][1].time, - output: undefined, - scope: 'CustomSettings', - observedUnit: 'status', - }, - ], - }); - expect(manager.error).toBeUndefined(); - expect(manager.metadata).toEqual({ - name: 'mdf-app', - release: '0.0.0', - version: '0', - description: undefined, - instanceId: manager.componentId, - }); - expect(manager.namespace).toBeUndefined(); - expect(manager.release).toEqual('0.0.0'); - expect(manager.observability).toEqual({ - metadata: { - name: 'mdf-app', - release: '0.0.0', - version: '0', - description: undefined, - instanceId: manager.componentId, - }, - service: { - primaryPort: 9081, - host: 'localhost', - isCluster: false, - includeStack: false, - clusterUpdateInterval: 10000, - maxSize: 100, - }, - }); - expect(manager.logger).toEqual({ - console: { - enabled: true, - level: 'info', - }, - file: { - enabled: false, - level: 'info', - }, - }); - expect(manager.isPrimary).toBeFalsy(); - expect(manager.isWorker).toBeFalsy(); - expect(manager.serviceRegisterConfigManager).toBeDefined(); - expect(manager.serviceRegistrySettings).toBeDefined(); - expect(manager.customRegisterConfigManager).toBeDefined(); - expect(manager.customSettings).toEqual({}); - expect(manager.settings).toEqual({ - metadata: { - name: 'mdf-app', - release: '0.0.0', - version: '0', - description: undefined, - instanceId: manager.componentId, - }, - retryOptions: { - attempts: 3, - maxWaitTime: 10000, - timeout: 5000, - waitTime: 1000, - }, - observabilityOptions: { - primaryPort: 9081, - host: 'localhost', - isCluster: false, - includeStack: false, - clusterUpdateInterval: 10000, - maxSize: 100, - }, - loggerOptions: { - console: { - enabled: true, - level: 'info', - }, - file: { - enabled: false, - level: 'info', - }, - }, - configLoaderOptions: { - configFiles: ['./config/custom/*.*'], - presetFiles: ['./config/custom/presets/*.*'], - schemaFiles: ['./config/custom/schemas/*.*'], - preset: undefined, - }, - custom: {}, - }); - expect(manager.retryOptions).toEqual({ - attempts: 3, - maxWaitTime: 10000, - timeout: 5000, - waitTime: 1000, - }); - expect(manager.router).toBeDefined(); - expect(manager.links).toEqual({ - settings: { - config: '/settings/config', - presets: '/settings/presets', - readme: '/settings/readme', - }, - }); - await manager.start(); - expect(manager.status).toEqual('pass'); - await manager.stop(); - await manager.close(); - }); - }); - describe('#Sad path', () => { - it(`Should create a valid instance of SettingsManager when there is a problem reading the "package.json" file`, async () => { - jest - .spyOn(fs, 'readFileSync') - .mockImplementationOnce((value: any) => { - throw new Crash('Error reading package.json'); - }) - .mockImplementationOnce((value: any) => { - throw new Multi('Error reading readme.json', { causes: [new Crash(`myError`)] }); - }); - const manager = new SettingsManager({ - loadPackage: true, - loadReadme: true, - useEnvironment: true, - }); - expect(manager).toBeInstanceOf(SettingsManager); - expect(manager.error).toBeDefined(); - expect((manager.error as Multi).causes?.length).toEqual(2); - expect(manager.error?.trace()).toEqual([ - 'CrashError: Error loading README.md info: Error reading package.json', - 'caused by CrashError: Error reading package.json', - 'CrashError: Error loading package info: Error reading readme.json', - 'caused by MultiError: Error reading readme.json', - 'failed with CrashError: myError', - ]); - }); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash, Multi } from '@mdf.js/crash'; +import fs from 'fs'; +import { SettingsManager } from '.'; +describe('#SettingsManager', () => { + describe('#Happy path', () => { + beforeEach(() => {}); + it(`Should create a valid instance of SettingsManager with default values`, async () => { + const manager = new SettingsManager(); + expect(manager).toBeInstanceOf(SettingsManager); + expect(manager.name).toEqual('settings'); + expect(manager.componentId).toBeDefined(); + expect(manager.status).toEqual('warn'); + const checks = manager.checks; + expect(checks).toEqual({ + 'mdf-app:settings': [ + { + status: 'warn', + componentId: manager.componentId, + componentType: 'setup service', + observedValue: 'stopped', + time: checks['mdf-app:settings'][0].time, + output: undefined, + scope: 'ServiceRegistry', + observedUnit: 'status', + }, + { + status: 'warn', + componentId: manager.componentId, + componentType: 'setup service', + observedValue: 'stopped', + time: checks['mdf-app:settings'][1].time, + output: undefined, + scope: 'CustomSettings', + observedUnit: 'status', + }, + ], + }); + expect(manager.error).toBeUndefined(); + expect(manager.metadata).toEqual({ + name: 'mdf-app', + release: '0.0.0', + version: '0', + description: undefined, + instanceId: manager.componentId, + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + }); + expect(manager.namespace).toBeUndefined(); + expect(manager.release).toEqual('0.0.0'); + expect(manager.observability).toEqual({ + metadata: { + name: 'mdf-app', + release: '0.0.0', + version: '0', + description: undefined, + instanceId: manager.componentId, + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + }, + service: { + primaryPort: 9081, + host: 'localhost', + isCluster: false, + includeStack: false, + clusterUpdateInterval: 10000, + maxSize: 100, + }, + }); + expect(manager.logger).toEqual({ + console: { + enabled: true, + level: 'info', + }, + file: { + enabled: false, + level: 'info', + }, + }); + expect(manager.isPrimary).toBeFalsy(); + expect(manager.isWorker).toBeFalsy(); + expect(manager.serviceRegisterConfigManager).toBeDefined(); + expect(manager.serviceRegistrySettings).toBeDefined(); + expect(manager.customRegisterConfigManager).toBeDefined(); + expect(manager.customSettings).toEqual({}); + expect(manager.settings).toEqual({ + metadata: { + name: 'mdf-app', + release: '0.0.0', + version: '0', + description: undefined, + instanceId: manager.componentId, + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', + }, + retryOptions: { + attempts: 3, + maxWaitTime: 10000, + timeout: 5000, + waitTime: 1000, + }, + observabilityOptions: { + primaryPort: 9081, + host: 'localhost', + isCluster: false, + includeStack: false, + clusterUpdateInterval: 10000, + maxSize: 100, + }, + loggerOptions: { + console: { + enabled: true, + level: 'info', + }, + file: { + enabled: false, + level: 'info', + }, + }, + configLoaderOptions: { + configFiles: ['./config/custom/*.*'], + presetFiles: ['./config/custom/presets/*.*'], + schemaFiles: ['./config/custom/schemas/*.*'], + preset: undefined, + }, + custom: {}, + }); + expect(manager.retryOptions).toEqual({ + attempts: 3, + maxWaitTime: 10000, + timeout: 5000, + waitTime: 1000, + }); + expect(manager.router).toBeDefined(); + expect(manager.links).toEqual({ + settings: { + config: '/settings/config', + presets: '/settings/presets', + readme: '/settings/readme', + }, + }); + await manager.start(); + expect(manager.status).toEqual('pass'); + await manager.stop(); + await manager.close(); + }); + }); + describe('#Sad path', () => { + it(`Should create a valid instance of SettingsManager when there is a problem reading the "package.json" file`, async () => { + jest + .spyOn(fs, 'readFileSync') + .mockImplementationOnce((value: any) => { + throw new Crash('Error reading package.json'); + }) + .mockImplementationOnce((value: any) => { + throw new Multi('Error reading readme.json', { causes: [new Crash(`myError`)] }); + }); + const manager = new SettingsManager({ + loadPackage: true, + loadReadme: true, + useEnvironment: true, + }); + expect(manager).toBeInstanceOf(SettingsManager); + expect(manager.error).toBeDefined(); + expect((manager.error as Multi).causes?.length).toEqual(2); + expect(manager.error?.trace()).toEqual([ + 'CrashError: Error loading README.md info: Error reading package.json', + 'caused by CrashError: Error reading package.json', + 'CrashError: Error loading package info: Error reading readme.json', + 'caused by MultiError: Error reading readme.json', + 'failed with CrashError: myError', + ]); + }); + }); +}); diff --git a/packages/components/service-registry/src/settings/types/const.ts b/packages/components/service-registry/src/settings/types/const.ts index 1c648157..c9637da8 100644 --- a/packages/components/service-registry/src/settings/types/const.ts +++ b/packages/components/service-registry/src/settings/types/const.ts @@ -18,17 +18,40 @@ import { } from '../../observability'; import { ServiceRegistrySettings } from '../../types'; +/** + * Custom config preset selector, used to load a specific preset from the custom config folder. + * Default files to search for are `./config/custom/presets/*.preset.*` + * This preset is used for the custom config. + * @defaultValue undefined + */ +const CONFIG_CUSTOM_PRESET = process.env['CONFIG_CUSTOM_PRESET']; +/** + * Service registry preset selector, used to load a specific preset from the service registry config folder. + * Default files to search for are `./config/presets/*.preset.*` + * This preset is used for the service registry config. + * @defaultValue undefined + */ +const CONFIG_SERVICE_REGISTRY_PRESET = process.env['CONFIG_SERVICE_REGISTRY_PRESET']; + +/** + * Application name + * @defaultValue 'mdf-app' + */ +const CONFIG_APP_NAME = process.env['CONFIG_APP_NAME']; + /** Health service name */ export const CONFIG_SERVICE_NAME = 'settings'; /** Default application metadata */ export const DEFAULT_APP_METADATA: Layer.App.Metadata = { - name: 'mdf-app', + name: CONFIG_APP_NAME ?? 'mdf-app', release: '0.0.0', version: '0', description: undefined, // This is a placeholder, the actual value will be set by the service registry instanceId: '00000000-0000-0000-0000-000000000000', + serviceId: 'mdf-service', + serviceGroupId: 'mdf-service-group', }; /** Default retry options for service startup */ @@ -75,7 +98,7 @@ export const DEFAULT_CUSTOM_CONFIG_LOADER_OPTIONS: Setup.Config = { configFiles: ['./config/custom/*.*'], presetFiles: ['./config/custom/presets/*.*'], schemaFiles: ['./config/custom/schemas/*.*'], - preset: process.env['CONFIG_CUSTOM_PRESET'] || process.env['CONFIG_SERVICE_REGISTRY_PRESET'], + preset: CONFIG_CUSTOM_PRESET ?? CONFIG_SERVICE_REGISTRY_PRESET, }; /** Default service registry config loader options */ @@ -83,7 +106,7 @@ export const DEFAULT_SERVICE_REGISTRY_CONFIG_CONFIG_LOADER_OPTIONS: Setup.Config configFiles: ['./config/*.*'], presetFiles: ['./config/presets/*.*'], schemaFiles: ['./config/schemas/*.*'], - preset: process.env['CONFIG_SERVICE_REGISTRY_PRESET'], + preset: CONFIG_SERVICE_REGISTRY_PRESET, }; /** Default service registry options */ @@ -94,4 +117,3 @@ export const DEFAULT_SERVICE_REGISTRY_OPTIONS: ServiceRegistrySettings = { loggerOptions: DEFAULT_LOGGER_OPTIONS, configLoaderOptions: DEFAULT_CUSTOM_CONFIG_LOADER_OPTIONS, }; - diff --git a/packages/components/service-registry/src/types/BootstrapSettings.i.ts b/packages/components/service-registry/src/types/BootstrapSettings.i.ts index e949f8c5..7349f35e 100644 --- a/packages/components/service-registry/src/types/BootstrapSettings.i.ts +++ b/packages/components/service-registry/src/types/BootstrapSettings.i.ts @@ -1,90 +1,92 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export interface BootstrapOptions { - /** - * List of files with deploying options to be loaded. The entries could be a file path or - * glob pattern. It supports configurations in JSON, YAML, TOML, and .env file formats. - * @example `['./config/*.json']` - * @example `['./config/logger.json', './config/metadata.yaml']` - */ - configFiles?: string[]; - /** - * List of files with preset options to be loaded. The entries could be a file path or glob - * pattern. The first part of the file name will be used as the preset name. The file name - * should be in the format of `presetName.config.json` or `presetName.config.yaml`. The name of - * the preset will be used to merge different files in order to create a single preset. - * @example `['./config/presets/*.json']` - * @example `['./config/presets/*.json', './config/presets/*.yaml']` - * @example `['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml']` - */ - presetFiles?: string[]; - /** - * Preset to be used as configuration base, if none is indicated, or the indicated preset is - * not found, the configuration from the configuration files will be used. - */ - preset?: string; - /** - * Flag indicating that the environment configuration variables should be used. The configuration - * loaded by environment variables will be merged with the rest of the configuration, overriding - * the configuration from files, but not the configuration passed as argument to Service Registry. - * When option is set some filters are applied to the environment variables to avoid conflicts in - * the configuration. The filters are: - * - * - `CONFIG_METADATA_`: Application metadata configuration. - * - `CONFIG_OBSERVABILITY_`: Observability service configuration. - * - `CONFIG_LOGGER_`: Logger configuration. - * - `CONFIG_RETRY_OPTIONS_`: Retry options configuration. - * - `CONFIG_ADAPTER_`: Consumer adapter configuration. - * - * The loader expect environment configuration variables represented in `SCREAMING_SNAKE_CASE`, - * that will parsed to `camelCase` and merged with the rest of the configuration. The consumer - * adapter configuration is an exception, due to the kind of configuration, it should be provided - * by configuration parameters. - * - * @example - * ```sh - * CONFIG_METADATA_NAME=MyApp - * CONFIG_METADATA_LINKS__SELF=https://myapp.com - * CONFIG_OBSERVABILITY_PORT=8080 - * CONFIG_LOGGER__CONSOLE__LEVEL=info - * CONFIG_RETRY_OPTIONS_ATTEMPTS=3 - * CONFIG_ADAPTER_TYPE=redis - * ``` - */ - useEnvironment?: boolean; - /** - * Flag indicating that the package.json file should be loaded. If this flag is set to `true`, the - * the module will scale parent directories looking for a `package.json` file to load, if the file - * is found, the package information will be used to fullfil the `metadata` field. - * - `package.name` will be used as the `metadata.name`. - * - `package.version` will be used as the `metadata.version`, and the first part of the version - * will be used as the `metadata.release`. - * - `package.description` will be used as the `metadata.description`. - * - `package.keywords` will be used as the `metadata.tags`. - * - `package.config.${name}`, where `name` is the name of the configuration, will be used to find - * the rest of properties with the same name that in the metadata. - * This information will be merged with the rest of the configuration, overriding the - * configuration from files, but not the configuration passed as argument to Service Registry. - */ - loadPackage?: boolean; - /** - * Flag indicating that the README.md file should be loaded. If this flag is set to `true`, the - * module will scale parent directories looking for a `README.md` file to load, if the file is - * found, the README content will be exposed in the observability endpoints. - * If the flag is a string, the string will be used as the file name to look for. - */ - loadReadme?: boolean | string; - /** - * Flag indicating if the OpenC2 Consumer command interface should be enabled. The command - * interface is a set of commands that can be used to interact with the application. - * The commands are exposed in the observability endpoints and can be used to interact with the - * service, or, if a consumer adapter is configured, to interact with the service from a central - * controller. - */ - consumer?: boolean; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Bootstrap options */ +export interface BootstrapOptions { + /** + * List of files with deploying options to be loaded. The entries could be a file path or + * glob pattern. It supports configurations in JSON, YAML, TOML, and .env file formats. + * @example `['./config/*.json']` + * @example `['./config/logger.json', './config/metadata.yaml']` + */ + configFiles?: string[]; + /** + * List of files with preset options to be loaded. The entries could be a file path or glob + * pattern. The first part of the file name will be used as the preset name. The file name + * should be in the format of `presetName.config.json` or `presetName.config.yaml`. The name of + * the preset will be used to merge different files in order to create a single preset. + * @example `['./config/presets/*.json']` + * @example `['./config/presets/*.json', './config/presets/*.yaml']` + * @example `['./config/presets/*.json', './config/presets/*.yaml', './config/presets/*.yml']` + */ + presetFiles?: string[]; + /** + * Preset to be used as configuration base, if none is indicated, or the indicated preset is + * not found, the configuration from the configuration files will be used. + */ + preset?: string; + /** + * Flag indicating that the environment configuration variables should be used. The configuration + * loaded by environment variables will be merged with the rest of the configuration, overriding + * the configuration from files, but not the configuration passed as argument to Service Registry. + * When option is set some filters are applied to the environment variables to avoid conflicts in + * the configuration. The filters are: + * + * - `CONFIG_METADATA_`: Application metadata configuration. + * - `CONFIG_OBSERVABILITY_`: Observability service configuration. + * - `CONFIG_LOGGER_`: Logger configuration. + * - `CONFIG_RETRY_OPTIONS_`: Retry options configuration. + * - `CONFIG_ADAPTER_`: Consumer adapter configuration. + * + * The loader expect environment configuration variables represented in `SCREAMING_SNAKE_CASE`, + * that will parsed to `camelCase` and merged with the rest of the configuration. The consumer + * adapter configuration is an exception, due to the kind of configuration, it should be provided + * by configuration parameters. + * + * @example + * ```sh + * CONFIG_METADATA_NAME=MyApp + * CONFIG_METADATA_LINKS__SELF=https://myapp.com + * CONFIG_OBSERVABILITY_PORT=8080 + * CONFIG_LOGGER__CONSOLE__LEVEL=info + * CONFIG_RETRY_OPTIONS_ATTEMPTS=3 + * CONFIG_ADAPTER_TYPE=redis + * ``` + */ + useEnvironment?: boolean; + /** + * Flag indicating that the package.json file should be loaded. If this flag is set to `true`, the + * the module will scale parent directories looking for a `package.json` file to load, if the file + * is found, the package information will be used to fullfil the `metadata` field. + * - `package.name` will be used as the `metadata.name`. + * - `package.version` will be used as the `metadata.version`, and the first part of the version + * will be used as the `metadata.release`. + * - `package.description` will be used as the `metadata.description`. + * - `package.keywords` will be used as the `metadata.tags`. + * - `package.config.${name}`, where `name` is the name of the configuration, will be used to find + * the rest of properties with the same name that in the metadata. + * This information will be merged with the rest of the configuration, overriding the + * configuration from files, but not the configuration passed as argument to Service Registry. + */ + loadPackage?: boolean; + /** + * Flag indicating that the README.md file should be loaded. If this flag is set to `true`, the + * module will scale parent directories looking for a `README.md` file to load, if the file is + * found, the README content will be exposed in the observability endpoints. + * If the flag is a string, the string will be used as the file name to look for. + */ + loadReadme?: boolean | string; + /** + * Flag indicating if the OpenC2 Consumer command interface should be enabled. The command + * interface is a set of commands that can be used to interact with the application. + * The commands are exposed in the observability endpoints and can be used to interact with the + * service, or, if a consumer adapter is configured, to interact with the service from a central + * controller. + */ + consumer?: boolean; +} + diff --git a/packages/providers/amqp/README.md b/packages/providers/amqp/README.md index 82e1d887..d4628dd9 100644 --- a/packages/providers/amqp/README.md +++ b/packages/providers/amqp/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/providers/amqp/package.json b/packages/providers/amqp/package.json index 96e63964..715640f0 100644 --- a/packages/providers/amqp/package.json +++ b/packages/providers/amqp/package.json @@ -1,54 +1,54 @@ -{ - "name": "@mdf.js/amqp-provider", - "version": "0.0.1", - "description": "MMS - AMQP Port for Javascript/Typescript", - "keywords": [ - "NodeJS", - "provider", - "MMS", - "amqp" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/providers/amqp" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/elastic/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "joi": "^17.13.3", - "lodash": "^4.17.21", - "rhea-promise": "^3.0.3", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/amqp-provider", + "version": "0.0.1", + "description": "MMS - AMQP Port for Javascript/Typescript", + "keywords": [ + "NodeJS", + "provider", + "MMS", + "amqp" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/providers/amqp" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/elastic/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "rhea": "^3.0.3", + "rhea-promise": "^3.0.3", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/providers/amqp/src/Common/config/default.ts b/packages/providers/amqp/src/Common/config/default.ts index 1678dab8..583fe498 100644 --- a/packages/providers/amqp/src/Common/config/default.ts +++ b/packages/providers/amqp/src/Common/config/default.ts @@ -1,54 +1,55 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Config } from '../types'; -import { CONFIG_ARTIFACT_ID } from './utils'; - -// ************************************************************************************************* -// #region Default values - -/** - * Used as default container id, receiver name, sender name, etc. in cluster configurations. - * @defaultValue undefined - */ -export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; - -const DEFAULT_CONFIG_AMQP_USER_NAME = 'mdf-amqp'; -const DEFAULT_CONFIG_AMQP_HOST = '127.0.0.1'; -const DEFAULT_CONFIG_AMQP_PORT = 5672; -const DEFAULT_CONFIG_AMQP_TRANSPORT = 'tcp'; -const DEFAULT_CONFIG_AMQP_CONTAINER_ID = NODE_APP_INSTANCE || CONFIG_ARTIFACT_ID; -const DEFAULT_CONFIG_AMQP_RECONNECT = 5000; -const DEFAULT_CONFIG_AMQP_INITIAL_RECONNECT_DELAY = 30000; -const DEFAULT_CONFIG_AMQP_MAX_RECONNECT_DELAY = 10000; -const DEFAULT_CONFIG_AMQP_NON_FATAL_ERRORS = ['amqp:connection:forced']; - -const DEFAULT_CONFIG_IDLE_TIME_OUT = 5000; -const DEFAULT_RECONNECT_LIMIT = Number.MAX_SAFE_INTEGER; -const DEFAULT_KEEP_ALIVE = true; -const DEFAULT_KEEP_ALIVE_INITIAL_DELAY = 2000; -const DEFAULT_TIMEOUT = 10000; -const DEFAULT_ALL_ERRORS_NON_FATAL = true; - -export const defaultConfig: Config = { - username: DEFAULT_CONFIG_AMQP_USER_NAME, - host: DEFAULT_CONFIG_AMQP_HOST, - port: DEFAULT_CONFIG_AMQP_PORT, - transport: DEFAULT_CONFIG_AMQP_TRANSPORT, - container_id: DEFAULT_CONFIG_AMQP_CONTAINER_ID, - reconnect: DEFAULT_CONFIG_AMQP_RECONNECT, - initial_reconnect_delay: DEFAULT_CONFIG_AMQP_INITIAL_RECONNECT_DELAY, - max_reconnect_delay: DEFAULT_CONFIG_AMQP_MAX_RECONNECT_DELAY, - non_fatal_errors: DEFAULT_CONFIG_AMQP_NON_FATAL_ERRORS, - idle_time_out: DEFAULT_CONFIG_IDLE_TIME_OUT, - reconnect_limit: DEFAULT_RECONNECT_LIMIT, - keepAlive: DEFAULT_KEEP_ALIVE, - keepAliveInitialDelay: DEFAULT_KEEP_ALIVE_INITIAL_DELAY, - timeout: DEFAULT_TIMEOUT, - all_errors_non_fatal: DEFAULT_ALL_ERRORS_NON_FATAL, -}; -// #endregion +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Config } from '../types'; +import { CONFIG_ARTIFACT_ID } from './utils'; + +// ************************************************************************************************* +// #region Default values + +/** + * Used as default container id, receiver name, sender name, etc. in cluster configurations. + * @defaultValue undefined + */ +export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; + +const DEFAULT_CONFIG_AMQP_USER_NAME = 'mdf-amqp'; +const DEFAULT_CONFIG_AMQP_HOST = '127.0.0.1'; +const DEFAULT_CONFIG_AMQP_PORT = 5672; +const DEFAULT_CONFIG_AMQP_TRANSPORT = 'tcp'; +const DEFAULT_CONFIG_AMQP_CONTAINER_ID = NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID; +const DEFAULT_CONFIG_AMQP_RECONNECT = 5000; +const DEFAULT_CONFIG_AMQP_INITIAL_RECONNECT_DELAY = 30000; +const DEFAULT_CONFIG_AMQP_MAX_RECONNECT_DELAY = 10000; +const DEFAULT_CONFIG_AMQP_NON_FATAL_ERRORS = ['amqp:connection:forced']; + +const DEFAULT_CONFIG_IDLE_TIME_OUT = 5000; +const DEFAULT_RECONNECT_LIMIT = Number.MAX_SAFE_INTEGER; +const DEFAULT_KEEP_ALIVE = true; +const DEFAULT_KEEP_ALIVE_INITIAL_DELAY = 2000; +const DEFAULT_TIMEOUT = 10000; +const DEFAULT_ALL_ERRORS_NON_FATAL = true; + +export const defaultConfig: Config = { + username: DEFAULT_CONFIG_AMQP_USER_NAME, + host: DEFAULT_CONFIG_AMQP_HOST, + port: DEFAULT_CONFIG_AMQP_PORT, + transport: DEFAULT_CONFIG_AMQP_TRANSPORT, + container_id: DEFAULT_CONFIG_AMQP_CONTAINER_ID, + reconnect: DEFAULT_CONFIG_AMQP_RECONNECT, + initial_reconnect_delay: DEFAULT_CONFIG_AMQP_INITIAL_RECONNECT_DELAY, + max_reconnect_delay: DEFAULT_CONFIG_AMQP_MAX_RECONNECT_DELAY, + non_fatal_errors: DEFAULT_CONFIG_AMQP_NON_FATAL_ERRORS, + idle_time_out: DEFAULT_CONFIG_IDLE_TIME_OUT, + reconnect_limit: DEFAULT_RECONNECT_LIMIT, + keepAlive: DEFAULT_KEEP_ALIVE, + keepAliveInitialDelay: DEFAULT_KEEP_ALIVE_INITIAL_DELAY, + timeout: DEFAULT_TIMEOUT, + all_errors_non_fatal: DEFAULT_ALL_ERRORS_NON_FATAL, +}; +// #endregion + diff --git a/packages/providers/amqp/src/Receiver/Port.ts b/packages/providers/amqp/src/Receiver/Port.ts index 05791151..0a27bd8e 100644 --- a/packages/providers/amqp/src/Receiver/Port.ts +++ b/packages/providers/amqp/src/Receiver/Port.ts @@ -1,21 +1,23 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { LoggerInstance } from '@mdf.js/logger'; -import { Receiver } from '../Client'; -import { BasePort } from '../Common'; -import { Config, Receiver as RheaReceiver } from './types'; - -export class Port extends BasePort { - /** - * Implementation of functionalities of an AMQP Receiver port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, new Receiver(config)); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { LoggerInstance } from '@mdf.js/logger'; +import { Receiver } from '../Client'; +import { BasePort } from '../Common'; +import { Config, Receiver as RheaReceiver } from './types'; + +/** Implementation of an AMQP Receiver port instance */ +export class Port extends BasePort { + /** + * Implementation of functionalities of an AMQP Receiver port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, new Receiver(config)); + } +} + diff --git a/packages/providers/amqp/src/Receiver/config/default.ts b/packages/providers/amqp/src/Receiver/config/default.ts index 12853350..eb0b6288 100644 --- a/packages/providers/amqp/src/Receiver/config/default.ts +++ b/packages/providers/amqp/src/Receiver/config/default.ts @@ -23,7 +23,7 @@ const DEFAULT_CONFIG_AMQP_RECEIVER_AUTO_SETTLE = true; export const defaultConfig: Config = { ...commonDefaultConfig, receiver_options: { - name: NODE_APP_INSTANCE || CONFIG_ARTIFACT_ID, + name: NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID, rcv_settle_mode: DEFAULT_CONFIG_AMQP_RECEIVER_SETTLE_MODE, credit_window: DEFAULT_CONFIG_AMQP_RECEIVER_CREDIT_WINDOW, autoaccept: DEFAULT_CONFIG_AMQP_RECEIVER_AUTO_ACCEPT, @@ -31,4 +31,3 @@ export const defaultConfig: Config = { }, }; // #endregion - diff --git a/packages/providers/amqp/src/Receiver/index.ts b/packages/providers/amqp/src/Receiver/index.ts index 5da22ef9..77511f80 100644 --- a/packages/providers/amqp/src/Receiver/index.ts +++ b/packages/providers/amqp/src/Receiver/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Config, ProviderInstance as Provider, Receiver } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export type { Port } from './Port'; +export { Config, ProviderInstance as Provider, Receiver } from './types'; + diff --git a/packages/providers/amqp/src/Sender/Port.ts b/packages/providers/amqp/src/Sender/Port.ts index d165be98..d53ea0cf 100644 --- a/packages/providers/amqp/src/Sender/Port.ts +++ b/packages/providers/amqp/src/Sender/Port.ts @@ -1,21 +1,23 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { LoggerInstance } from '@mdf.js/logger'; -import { Sender } from '../Client'; -import { BasePort } from '../Common'; -import { Config, AwaitableSender as RheaSender } from './types'; - -export class Port extends BasePort { - /** - * Implementation of functionalities of an AMQP Sender port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, new Sender(config)); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { LoggerInstance } from '@mdf.js/logger'; +import { Sender } from '../Client'; +import { BasePort } from '../Common'; +import { Config, AwaitableSender as RheaSender } from './types'; + +/** Implementation of an AMQP Sender port instance */ +export class Port extends BasePort { + /** + * Implementation of functionalities of an AMQP Sender port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, new Sender(config)); + } +} + diff --git a/packages/providers/amqp/src/Sender/config/default.ts b/packages/providers/amqp/src/Sender/config/default.ts index 37abd5ce..fb4a285c 100644 --- a/packages/providers/amqp/src/Sender/config/default.ts +++ b/packages/providers/amqp/src/Sender/config/default.ts @@ -21,11 +21,10 @@ const DEFAULT_CONFIG_AMQP_SENDER_TARGET = {}; export const defaultConfig: Config = { ...commonDefaultConfig, sender_options: { - name: NODE_APP_INSTANCE || CONFIG_ARTIFACT_ID, + name: NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID, snd_settle_mode: DEFAULT_CONFIG_AMQP_SENDER_SETTLE_MODE, autosettle: DEFAULT_CONFIG_AMQP_SENDER_AUTO_SETTLE, target: DEFAULT_CONFIG_AMQP_SENDER_TARGET, }, }; // #endregion - diff --git a/packages/providers/amqp/src/Sender/index.ts b/packages/providers/amqp/src/Sender/index.ts index 74e64938..e19f2745 100644 --- a/packages/providers/amqp/src/Sender/index.ts +++ b/packages/providers/amqp/src/Sender/index.ts @@ -1,9 +1,10 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { AwaitableSender, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export type { Port } from './Port'; +export { AwaitableSender, Config, ProviderInstance as Provider } from './types'; diff --git a/packages/providers/amqp/src/base/protocol/Frame/index.ts b/packages/providers/amqp/src/base/protocol/Frame/index.ts index 0599f1ba..0778abd4 100644 --- a/packages/providers/amqp/src/base/protocol/Frame/index.ts +++ b/packages/providers/amqp/src/base/protocol/Frame/index.ts @@ -1,10 +1,7 @@ -/** - * Copyright 2024 Netin Systems S.L. All rights reserved. - * Note: All information contained herein is, and remains the property of Netin Systems S.L. and its - * suppliers, if any. The intellectual and technical concepts contained herein are property of - * Netin Systems S.L. and its suppliers and may be covered by European and Foreign patents, patents - * in process, and are protected by trade secret or copyright. - * - * Dissemination of this information or the reproduction of this material is strictly forbidden - * unless prior written permission is obtained from Netin Systems S.L. - */ +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + diff --git a/packages/providers/amqp/src/base/protocol/Primitives/Serializer/Parser.ts b/packages/providers/amqp/src/base/protocol/Primitives/Serializer/Parser.ts index 68bbbb9e..9be6bb75 100644 --- a/packages/providers/amqp/src/base/protocol/Primitives/Serializer/Parser.ts +++ b/packages/providers/amqp/src/base/protocol/Primitives/Serializer/Parser.ts @@ -1,315 +1,318 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import { parse } from 'uuid'; -import { Types } from '../..'; - -/** Serializer for AMQP elements */ -export class Parser { - /** - * Serializes the given value - * @param value The value to serialize - * @param primitive The type of the value - * @returns The serialized value - */ - public static parse(value: unknown, primitive: Types.Primitive): Buffer { - let buffer: Buffer; - switch (primitive) { - // size: 1, code: 0x41 - case Types.Primitive.TRUE: - // size: 1, code: 0x42 - case Types.Primitive.FALSE: - // size: 1, code: 0x40 - case Types.Primitive.NULL: - // size: 1, code: 0x43 - case Types.Primitive.UNIT0: - // size: 1, code: 0x44 - case Types.Primitive.ULONG0: - // size: 1, code: 0x45 - case Types.Primitive.LIST0: - buffer = Buffer.alloc(0); - break; - // size: 2, code: 0x56 - case Types.Primitive.BOOLEAN: - Parser.isBoolean(value); - buffer = Buffer.alloc(1); - buffer.writeUInt8(value ? Types.Primitive.TRUE : Types.Primitive.FALSE); - break; - // size: 2, code: 0x50 - case Types.Primitive.UBYTE: - // size: 2, code: 0x51 - case Types.Primitive.SMALL_UINT: - // size: 2, code: 0x52 - case Types.Primitive.SMALL_ULONG: - Parser.isRange(value, 0, 255); - buffer = Buffer.alloc(1); - buffer.writeUInt8(value); - break; - // size: 3, code: 0x61 - case Types.Primitive.USHORT: - Parser.isRange(value, 0, 65535); - buffer = Buffer.alloc(2); - buffer.writeUInt16BE(value as number); - break; - // size: 5, code: 0x71 - case Types.Primitive.UINT: - Parser.isRange(value, 0, 4294967295); - buffer = Buffer.alloc(4); - buffer.writeUInt32BE(value); - break; - // size: 9, code: 0x81 - case Types.Primitive.ULONG: - const bigIntValue = Parser.getAsBigInt(value); - if (bigIntValue < BigInt(0)) { - throw new Crash(`Expected a positive number, got ${bigIntValue}`, { - name: 'ProtocolError', - }); - } - buffer = Buffer.alloc(8); - buffer.writeBigUInt64BE(bigIntValue); - break; - // size: 5, code: 0x72 - case Types.Primitive.BYTE: - // size: 2, code: 0x54 - case Types.Primitive.SMALL_INT: - // size: 2, code: 0x55 - case Types.Primitive.SMALL_LONG: - Parser.isRange(value, -128, 127); - buffer = Buffer.alloc(1); - buffer.writeInt8(value); - break; - // size: 3, code: 0x61 - case Types.Primitive.SHORT: - Parser.isNumber(value); - Parser.isRange(value as number, -32768, 32767); - buffer = Buffer.alloc(2); - buffer.writeInt16BE(value); - break; - // size: 5, code: 0x71 - case Types.Primitive.INT: - Parser.isRange(value, -2147483648, 2147483647); - buffer = Buffer.alloc(4); - buffer.writeInt32BE(value); - break; - // size: 9, code: 0x81 - case Types.Primitive.LONG: - const bigintValue: bigint = Parser.getAsBigInt(value); - buffer = Buffer.alloc(8); - buffer.writeBigInt64BE(bigintValue); - break; - // size: 5, code: 0x72 - case Types.Primitive.FLOAT: - Parser.isFloat(value); - buffer = Buffer.alloc(4); - buffer.writeFloatBE(value); - break; - // size: 9, code: 0x82 - case Types.Primitive.DOUBLE: - Parser.isFloat(value); - buffer = Buffer.alloc(8); - buffer.writeDoubleBE(value); - break; - // size: 5, code: 0x73 - case Types.Primitive.DECIMAL32: - // size: 9, code: 0x83 - case Types.Primitive.DECIMAL64: - // size: 17, code: 0x94 - case Types.Primitive.DECIMAL128: - Parser.notImplemented(primitive); - // size: 5, code: 0x73 - case Types.Primitive.CHAR: - Parser.isString(value, 1); - buffer = Buffer.alloc(4); - buffer.writeUInt32BE(value.charCodeAt(0)); - break; - // size: 5, code: 0x83 - case Types.Primitive.TIMESTAMP: - if (!(value instanceof Date)) { - throw new Crash(`Expected a date, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - const _time = BigInt(value.getTime()); - buffer = Buffer.alloc(8); - buffer.writeBigUInt64BE(_time); - break; - // size: 17, code: 0x98 - case Types.Primitive.UUID: - Parser.isString(value, 36); - buffer = Buffer.from(parse(value)); - break; - // size: 2 + value length, code: 0xa0 - case Types.Primitive.VBIN8: - Parser.isBuffer(value, 2 ** 8 - 1); - buffer = Buffer.alloc(1 + value.length); - buffer.writeUInt8(value.length); - value.copy(buffer, 1); - break; - // size: 5 + value.length, code: 0xb0 - case Types.Primitive.VBIN32: - Parser.isBuffer(value, 2 ** 32 - 1); - buffer = Buffer.alloc(4 + value.length); - buffer.writeUInt32BE(value.length); - value.copy(buffer, 4); - break; - // size: 2 + value.length, code: 0xa1 - case Types.Primitive.STR8: - Parser.isString(value, 2 ** 8 - 1); - buffer = Buffer.alloc(1 + value.length); - buffer.writeUInt8(value.length); - buffer.write(value, 1, 'utf-8'); - break; - // size: 5 + value.length, code: 0xb1 - case Types.Primitive.STR32: - Parser.isString(value, 2 ** 32 - 1); - buffer = Buffer.alloc(4 + value.length); - buffer.writeUInt32BE(value.length); - buffer.write(value, 4, 'utf-8'); - break; - // size: 2 + value.length, code: 0xa3 - case Types.Primitive.SYM8: - Parser.isString(value, 2 ** 8 - 1); - buffer = Buffer.alloc(1 + value.length); - buffer.writeUInt8(value.length); - buffer.write(value, 1, 'ascii'); - break; - // size: 5 + value.length, code: 0xb3 - case Types.Primitive.SYM32: - Parser.isString(value, 2 ** 32 - 1); - buffer = Buffer.alloc(4 + value.length); - buffer.writeUInt32BE(value.length); - buffer.write(value, 4, 'ascii'); - break; - // size: 2 + value.length, code: 0xc0 - default: - throw new Crash( - `Not parsable code [0x${primitive.toString(16)}]/[${Types.Primitive[primitive]}], parsing failed.`, - { name: 'ProtocolError' } - ); - } - return buffer; - } - /** - * Throws a crash error for a not implemented primitive - * @param primitive - The primitive that is not implemented - * @throws Crash - The crash error - */ - private static notImplemented(primitive: Types.Primitive): never { - throw new Crash(`Decoder for ${Types.Primitive[primitive]} is not implemented`, { - name: 'NotImplemented', - }); - } - /** - * Checks if the value is a number - * @param value - value to be checked - * @throws Crash - if the value is not a number - */ - private static isNumber(value: unknown): asserts value is number { - if (typeof value !== 'number') { - throw new Crash(`Expected a number, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - } - /** - * Checks if the value is within the given range - * @param value - value to be checked - * @param min - minimum value - * @param max - maximum value - * @throws Crash - if the value is not within the given range - */ - private static isRange(value: unknown, min: number, max: number): asserts value is number { - Parser.isNumber(value); - if (value < min || value > max) { - throw new Crash(`Expected a number between ${min} and ${max}, got ${value}`, { - name: 'ProtocolError', - }); - } - } - /** - * Checks if the value is a float number - * @param value - value to be checked - * @throws Crash - if the value is not a float number - */ - private static isFloat(value: unknown): asserts value is number { - Parser.isNumber(value); - if (!Number.isFinite(value) || Number.isInteger(value)) { - throw new Crash(`Expected a float number, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - } - /** - * Return the value as a bigint - * @param value - value to be converted - * @throws Crash - if the value is not a number or bigint - */ - private static getAsBigInt(value: unknown): bigint { - if (typeof value === 'bigint') { - return value; - } else if (value instanceof BigInt) { - return value.valueOf(); - } else if (typeof value === 'number') { - return BigInt(value); - } else { - throw new Crash(`Expected a number or bigint, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - } - /** - * Checks if the value is a string - * @param value - value to be checked - * @param maxSize - maximum size of the string - * @throws Crash - if the value is not a string - */ - private static isString(value: unknown, maxSize: number): asserts value is string { - if (typeof value !== 'string') { - throw new Crash(`Expected a string, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - if (maxSize !== undefined && value.length > maxSize) { - throw new Crash( - `Expected a string with a maximum length of ${maxSize}, got ${value.length}`, - { name: 'ProtocolError' } - ); - } - } - /** - * Checks if the value is a boolean - * @param value - value to be checked - * @throws Crash - if the value is not a boolean - */ - private static isBoolean(value: unknown): asserts value is boolean { - if (typeof value !== 'boolean') { - throw new Crash(`Expected a boolean, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - } - /** - * Checks if the value is a buffer - * @param value - value to be checked - * @param maxSize - maximum size of the buffer - * @throws Crash - if the value is not a buffer - */ - private static isBuffer(value: unknown, maxSize: number): asserts value is Buffer { - if (!Buffer.isBuffer(value)) { - throw new Crash(`Expected a buffer, got ${typeof value}`, { - name: 'ProtocolError', - }); - } - if (maxSize !== undefined && value.length > maxSize) { - throw new Crash( - `Expected a buffer with a maximum length of ${maxSize}, got ${value.length}`, - { name: 'ProtocolError' } - ); - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { parse } from 'uuid'; +import { Types } from '../..'; + +/** Serializer for AMQP elements */ +export class Parser { + /** + * Serializes the given value + * @param value The value to serialize + * @param primitive The type of the value + * @returns The serialized value + */ + public static parse(value: unknown, primitive: Types.Primitive): Buffer { + let buffer: Buffer; + switch (primitive) { + // size: 1, code: 0x41 + case Types.Primitive.TRUE: + // size: 1, code: 0x42 + case Types.Primitive.FALSE: + // size: 1, code: 0x40 + case Types.Primitive.NULL: + // size: 1, code: 0x43 + case Types.Primitive.UNIT0: + // size: 1, code: 0x44 + case Types.Primitive.ULONG0: + // size: 1, code: 0x45 + case Types.Primitive.LIST0: + buffer = Buffer.alloc(0); + break; + // size: 2, code: 0x56 + case Types.Primitive.BOOLEAN: + Parser.isBoolean(value); + buffer = Buffer.alloc(1); + buffer.writeUInt8(value ? Types.Primitive.TRUE : Types.Primitive.FALSE); + break; + // size: 2, code: 0x50 + case Types.Primitive.UBYTE: + // size: 2, code: 0x51 + case Types.Primitive.SMALL_UINT: + // size: 2, code: 0x52 + case Types.Primitive.SMALL_ULONG: + Parser.isRange(value, 0, 255); + buffer = Buffer.alloc(1); + buffer.writeUInt8(value); + break; + // size: 3, code: 0x61 + case Types.Primitive.USHORT: + Parser.isRange(value, 0, 65535); + buffer = Buffer.alloc(2); + buffer.writeUInt16BE(value); + break; + // size: 5, code: 0x71 + case Types.Primitive.UINT: + Parser.isRange(value, 0, 4294967295); + buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value); + break; + // size: 9, code: 0x81 + case Types.Primitive.ULONG: { + const bigIntValue = Parser.getAsBigInt(value); + if (bigIntValue < BigInt(0)) { + throw new Crash(`Expected a positive number, got ${bigIntValue}`, { + name: 'ProtocolError', + }); + } + buffer = Buffer.alloc(8); + buffer.writeBigUInt64BE(bigIntValue); + break; + } + // size: 5, code: 0x72 + case Types.Primitive.BYTE: + // size: 2, code: 0x54 + case Types.Primitive.SMALL_INT: + // size: 2, code: 0x55 + case Types.Primitive.SMALL_LONG: + Parser.isRange(value, -128, 127); + buffer = Buffer.alloc(1); + buffer.writeInt8(value); + break; + // size: 3, code: 0x61 + case Types.Primitive.SHORT: + Parser.isNumber(value); + Parser.isRange(value, -32768, 32767); + buffer = Buffer.alloc(2); + buffer.writeInt16BE(value); + break; + // size: 5, code: 0x71 + case Types.Primitive.INT: + Parser.isRange(value, -2147483648, 2147483647); + buffer = Buffer.alloc(4); + buffer.writeInt32BE(value); + break; + // size: 9, code: 0x81 + case Types.Primitive.LONG: { + const bigintValue: bigint = Parser.getAsBigInt(value); + buffer = Buffer.alloc(8); + buffer.writeBigInt64BE(bigintValue); + break; + } + // size: 5, code: 0x72 + case Types.Primitive.FLOAT: + Parser.isFloat(value); + buffer = Buffer.alloc(4); + buffer.writeFloatBE(value); + break; + // size: 9, code: 0x82 + case Types.Primitive.DOUBLE: + Parser.isFloat(value); + buffer = Buffer.alloc(8); + buffer.writeDoubleBE(value); + break; + // size: 5, code: 0x73 + case Types.Primitive.DECIMAL32: + // size: 9, code: 0x83 + case Types.Primitive.DECIMAL64: + // size: 17, code: 0x94 + case Types.Primitive.DECIMAL128: + Parser.notImplemented(primitive); + // size: 5, code: 0x73 + case Types.Primitive.CHAR: + Parser.isString(value, 1); + buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value.charCodeAt(0)); + break; + // size: 5, code: 0x83 + case Types.Primitive.TIMESTAMP: { + if (!(value instanceof Date)) { + throw new Crash(`Expected a date, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + const _time = BigInt(value.getTime()); + buffer = Buffer.alloc(8); + buffer.writeBigUInt64BE(_time); + break; + } + // size: 17, code: 0x98 + case Types.Primitive.UUID: + Parser.isString(value, 36); + buffer = Buffer.from(parse(value)); + break; + // size: 2 + value length, code: 0xa0 + case Types.Primitive.VBIN8: + Parser.isBuffer(value, 2 ** 8 - 1); + buffer = Buffer.alloc(1 + value.length); + buffer.writeUInt8(value.length); + value.copy(buffer, 1); + break; + // size: 5 + value.length, code: 0xb0 + case Types.Primitive.VBIN32: + Parser.isBuffer(value, 2 ** 32 - 1); + buffer = Buffer.alloc(4 + value.length); + buffer.writeUInt32BE(value.length); + value.copy(buffer, 4); + break; + // size: 2 + value.length, code: 0xa1 + case Types.Primitive.STR8: + Parser.isString(value, 2 ** 8 - 1); + buffer = Buffer.alloc(1 + value.length); + buffer.writeUInt8(value.length); + buffer.write(value, 1, 'utf-8'); + break; + // size: 5 + value.length, code: 0xb1 + case Types.Primitive.STR32: + Parser.isString(value, 2 ** 32 - 1); + buffer = Buffer.alloc(4 + value.length); + buffer.writeUInt32BE(value.length); + buffer.write(value, 4, 'utf-8'); + break; + // size: 2 + value.length, code: 0xa3 + case Types.Primitive.SYM8: + Parser.isString(value, 2 ** 8 - 1); + buffer = Buffer.alloc(1 + value.length); + buffer.writeUInt8(value.length); + buffer.write(value, 1, 'ascii'); + break; + // size: 5 + value.length, code: 0xb3 + case Types.Primitive.SYM32: + Parser.isString(value, 2 ** 32 - 1); + buffer = Buffer.alloc(4 + value.length); + buffer.writeUInt32BE(value.length); + buffer.write(value, 4, 'ascii'); + break; + // size: 2 + value.length, code: 0xc0 + default: + throw new Crash( + `Not parsable code [0x${primitive.toString(16)}]/[${Types.Primitive[primitive]}], parsing failed.`, + { name: 'ProtocolError' } + ); + } + return buffer; + } + /** + * Throws a crash error for a not implemented primitive + * @param primitive - The primitive that is not implemented + * @throws Crash - The crash error + */ + private static notImplemented(primitive: Types.Primitive): never { + throw new Crash(`Decoder for ${Types.Primitive[primitive]} is not implemented`, { + name: 'NotImplemented', + }); + } + /** + * Checks if the value is a number + * @param value - value to be checked + * @throws Crash - if the value is not a number + */ + private static isNumber(value: unknown): asserts value is number { + if (typeof value !== 'number') { + throw new Crash(`Expected a number, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + } + /** + * Checks if the value is within the given range + * @param value - value to be checked + * @param min - minimum value + * @param max - maximum value + * @throws Crash - if the value is not within the given range + */ + private static isRange(value: unknown, min: number, max: number): asserts value is number { + Parser.isNumber(value); + if (value < min || value > max) { + throw new Crash(`Expected a number between ${min} and ${max}, got ${value}`, { + name: 'ProtocolError', + }); + } + } + /** + * Checks if the value is a float number + * @param value - value to be checked + * @throws Crash - if the value is not a float number + */ + private static isFloat(value: unknown): asserts value is number { + Parser.isNumber(value); + if (!Number.isFinite(value) || Number.isInteger(value)) { + throw new Crash(`Expected a float number, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + } + /** + * Return the value as a bigint + * @param value - value to be converted + * @throws Crash - if the value is not a number or bigint + */ + private static getAsBigInt(value: unknown): bigint { + if (typeof value === 'bigint') { + return value; + } else if (value instanceof BigInt) { + return value.valueOf(); + } else if (typeof value === 'number') { + return BigInt(value); + } else { + throw new Crash(`Expected a number or bigint, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + } + /** + * Checks if the value is a string + * @param value - value to be checked + * @param maxSize - maximum size of the string + * @throws Crash - if the value is not a string + */ + private static isString(value: unknown, maxSize: number): asserts value is string { + if (typeof value !== 'string') { + throw new Crash(`Expected a string, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + if (maxSize !== undefined && value.length > maxSize) { + throw new Crash( + `Expected a string with a maximum length of ${maxSize}, got ${value.length}`, + { name: 'ProtocolError' } + ); + } + } + /** + * Checks if the value is a boolean + * @param value - value to be checked + * @throws Crash - if the value is not a boolean + */ + private static isBoolean(value: unknown): asserts value is boolean { + if (typeof value !== 'boolean') { + throw new Crash(`Expected a boolean, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + } + /** + * Checks if the value is a buffer + * @param value - value to be checked + * @param maxSize - maximum size of the buffer + * @throws Crash - if the value is not a buffer + */ + private static isBuffer(value: unknown, maxSize: number): asserts value is Buffer { + if (!Buffer.isBuffer(value)) { + throw new Crash(`Expected a buffer, got ${typeof value}`, { + name: 'ProtocolError', + }); + } + if (maxSize !== undefined && value.length > maxSize) { + throw new Crash( + `Expected a buffer with a maximum length of ${maxSize}, got ${value.length}`, + { name: 'ProtocolError' } + ); + } + } +} diff --git a/packages/providers/amqp/src/base/protocol/Version/Handler.ts b/packages/providers/amqp/src/base/protocol/Version/Handler.ts index c50aea15..bf1ef7f6 100644 --- a/packages/providers/amqp/src/base/protocol/Version/Handler.ts +++ b/packages/providers/amqp/src/base/protocol/Version/Handler.ts @@ -1,34 +1,31 @@ -/** - * Copyright 2024 Netin Systems S.L. All rights reserved. - * Note: All information contained herein is, and remains the property of Netin Systems S.L. and its - * suppliers, if any. The intellectual and technical concepts contained herein are property of - * Netin Systems S.L. and its suppliers and may be covered by European and Foreign patents, patents - * in process, and are protected by trade secret or copyright. - * - * Dissemination of this information or the reproduction of this material is strictly forbidden - * unless prior written permission is obtained from Netin Systems S.L. - */ - -import { Packets } from '../types'; -import { Deserializer } from './Deserializer'; -import { Serializer } from './Serializer'; - -/** Handler for the Version packet */ -export class Handler { - /** - * Deserialize a buffer into a Version object - * @param buffer - buffer to be deserialized - * @returns a Version object - */ - public static deserialize(buffer: Buffer): Packets.Version { - return new Deserializer(buffer); - } - /** - * Serialize a Version object into a buffer - * @param packet - packet to be serialized - * @returns a buffer - */ - public static serialize(packet: Packets.Version): Buffer { - return new Serializer(packet).toBuffer(); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Packets } from '../types'; +import { Deserializer } from './Deserializer'; +import { Serializer } from './Serializer'; + +/** Handler for the Version packet */ +export class Handler { + /** + * Deserialize a buffer into a Version object + * @param buffer - buffer to be deserialized + * @returns a Version object + */ + public static deserialize(buffer: Buffer): Packets.Version { + return new Deserializer(buffer); + } + /** + * Serialize a Version object into a buffer + * @param packet - packet to be serialized + * @returns a buffer + */ + public static serialize(packet: Packets.Version): Buffer { + return new Serializer(packet).toBuffer(); + } +} + diff --git a/packages/providers/amqp/src/base/protocol/Version/Serializer.ts b/packages/providers/amqp/src/base/protocol/Version/Serializer.ts index 124fdb26..9b6e2a96 100644 --- a/packages/providers/amqp/src/base/protocol/Version/Serializer.ts +++ b/packages/providers/amqp/src/base/protocol/Version/Serializer.ts @@ -1,38 +1,39 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Packets } from '../types'; - -const VERSION_HEADER_SIZE = 8; -const VERSION_NAME_OFFSET = 0; -const VERSION_NAME_SIZE = 4; -const VERSION_PROTOCOL_ID_OFFSET = 4; -const VERSION_MAJOR_OFFSET = 5; -const VERSION_MINOR_OFFSET = 6; -const VERSION_REVISION_OFFSET = 7; - -/** Serializer for the Version packet */ -export class Serializer { - /** Buffer to store the serialized data */ - private buffer: Buffer; - /** - * Serialize a Version object into a buffer - * @param packet - packet to be serialized - */ - constructor(packet: Packets.Version) { - this.buffer = Buffer.alloc(VERSION_HEADER_SIZE); - this.buffer.write(packet.name, VERSION_NAME_OFFSET, VERSION_NAME_SIZE, 'ascii'); - this.buffer.writeUInt8(packet.protocolId, VERSION_PROTOCOL_ID_OFFSET); - this.buffer.writeUInt8(packet.major, VERSION_MAJOR_OFFSET); - this.buffer.writeUInt8(packet.minor, VERSION_MINOR_OFFSET); - this.buffer.writeUInt8(packet.revision, VERSION_REVISION_OFFSET); - } - /** Return the buffer representation */ - toBuffer(): Buffer { - return this.buffer; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Packets } from '../types'; + +const VERSION_HEADER_SIZE = 8; +const VERSION_NAME_OFFSET = 0; +const VERSION_NAME_SIZE = 4; +const VERSION_PROTOCOL_ID_OFFSET = 4; +const VERSION_MAJOR_OFFSET = 5; +const VERSION_MINOR_OFFSET = 6; +const VERSION_REVISION_OFFSET = 7; + +/** Serializer for the Version packet */ +export class Serializer { + /** Buffer to store the serialized data */ + private readonly buffer: Buffer; + /** + * Serialize a Version object into a buffer + * @param packet - packet to be serialized + */ + constructor(packet: Packets.Version) { + this.buffer = Buffer.alloc(VERSION_HEADER_SIZE); + this.buffer.write(packet.name, VERSION_NAME_OFFSET, VERSION_NAME_SIZE, 'ascii'); + this.buffer.writeUInt8(packet.protocolId, VERSION_PROTOCOL_ID_OFFSET); + this.buffer.writeUInt8(packet.major, VERSION_MAJOR_OFFSET); + this.buffer.writeUInt8(packet.minor, VERSION_MINOR_OFFSET); + this.buffer.writeUInt8(packet.revision, VERSION_REVISION_OFFSET); + } + /** Return the buffer representation */ + toBuffer(): Buffer { + return this.buffer; + } +} + diff --git a/packages/providers/amqp/src/base/protocol/Version/index.ts b/packages/providers/amqp/src/base/protocol/Version/index.ts index e9b913e1..7a3c1cf1 100644 --- a/packages/providers/amqp/src/base/protocol/Version/index.ts +++ b/packages/providers/amqp/src/base/protocol/Version/index.ts @@ -1,12 +1,9 @@ -/** - * Copyright 2024 Netin Systems S.L. All rights reserved. - * Note: All information contained herein is, and remains the property of Netin Systems S.L. and its - * suppliers, if any. The intellectual and technical concepts contained herein are property of - * Netin Systems S.L. and its suppliers and may be covered by European and Foreign patents, patents - * in process, and are protected by trade secret or copyright. - * - * Dissemination of this information or the reproduction of this material is strictly forbidden - * unless prior written permission is obtained from Netin Systems S.L. - */ - -export * from './Handler'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Handler'; + diff --git a/packages/providers/elastic/README.md b/packages/providers/elastic/README.md index 75431a65..305b565d 100644 --- a/packages/providers/elastic/README.md +++ b/packages/providers/elastic/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -156,7 +157,7 @@ Checks included in the provider: - **CONFIG\_ELASTIC\_TLS\_SERVER\_NAME** (default: `undefined`): Server name for the TLS certificate. - **CONFIG\_ELASTIC\_AUTH\_USERNAME** (default: `undefined`): Username for the Elasticsearch cluster. If this is set, a password must also be provided. - **CONFIG\_ELASTIC\_AUTH\_PASSWORD** (default: `undefined`): Password for the Elasticsearch cluster. If this is set, a username must also be provided. -- **NODE\_APP\_INSTANCE**: undefined +- **NODE\_APP\_INSTANCE** (default: `undefined`): Used as default container id, receiver name, sender name, etc. in cluster configurations. ## **License** diff --git a/packages/providers/elastic/package.json b/packages/providers/elastic/package.json index 689567ba..30bc4b8d 100644 --- a/packages/providers/elastic/package.json +++ b/packages/providers/elastic/package.json @@ -30,13 +30,13 @@ "test": "jest --detectOpenHandles --config ./jest.config.js" }, "dependencies": { - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^8.17.0", "@mdf.js/core": "*", "@mdf.js/crash": "*", "@mdf.js/logger": "*", "@mdf.js/utils": "*", "joi": "^17.13.3", - "tslib": "^2.7.0" + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*" diff --git a/packages/providers/elastic/src/config/default.ts b/packages/providers/elastic/src/config/default.ts index 9e2ef9a4..dc4008c8 100644 --- a/packages/providers/elastic/src/config/default.ts +++ b/packages/providers/elastic/src/config/default.ts @@ -10,12 +10,19 @@ import { CONFIG_ARTIFACT_ID } from './utils'; // ************************************************************************************************* // #region Default values + +/** + * Used as default container id, receiver name, sender name, etc. in cluster configurations. + * @defaultValue undefined + */ +export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; + const ELASTIC_NODES = ['http://localhost:9200']; const ELASTIC_MAX_RETRIES = 5; const ELASTIC_REQUEST_TIMEOUT = 30000; const ELASTIC_PING_TIMEOUT = 3000; const ELASTIC_RESURRECT_STRATEGY: 'ping' | 'optimistic' | 'none' = 'ping'; -const ELASTIC_NAME = process.env['NODE_APP_INSTANCE'] || CONFIG_ARTIFACT_ID; +const ELASTIC_NAME = NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID; export const defaultConfig: Config = { nodes: ELASTIC_NODES, @@ -26,4 +33,3 @@ export const defaultConfig: Config = { name: ELASTIC_NAME, }; // #endregion - diff --git a/packages/providers/elastic/src/index.ts b/packages/providers/elastic/src/index.ts index ff3905d9..0ec206c5 100644 --- a/packages/providers/elastic/src/index.ts +++ b/packages/providers/elastic/src/index.ts @@ -1,36 +1,8 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -/** - * Elastic Provider. - * As the rest of the providers, it is a wrapper around a third party library. The only exported - * item is namespace `Elastic` which contains the provider factory, besides some useful types to - * manage the provider. - * - * This means that the provider is not directly available, but it must be created using the factory - * function. - * - * @example - * ```typescript - * import { Elastic } from '@mdf.js/elastic-provider'; - * const provider = Elastic.Factory.create(); - * ``` - * `create` function accepts a configuration object as parameter, which is used to configure the - * provider. This configuration object is a {@link "@mdf.js/core" | Layer.Provider.FactoryOptions} - * object, which is a generic object that can be extended to add provider specific configuration - * options. - * - * In this case, the configuration object is a {@link Config} object, which is a type exported by - * the provider. This object is a {@link ClientOptions} object, which is a type exported by the - * third party library. - * - * This means that our {@link FactoryOptions} object has the next structure: - * @inheritdoc FactoryOptions - * - * @example - */ -export * as Elastic from './provider'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * as Elastic from './provider'; diff --git a/packages/providers/elastic/src/provider/Status.t.ts b/packages/providers/elastic/src/provider/Status.t.ts index 68d13640..c7355723 100644 --- a/packages/providers/elastic/src/provider/Status.t.ts +++ b/packages/providers/elastic/src/provider/Status.t.ts @@ -1,37 +1,39 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export type Status = { - /** Timestamp */ - epoch: number; - /** Timestamp */ - timestamp: string; - /** Health status */ - status: 'red' | 'yellow' | 'green'; - /** Cluster name */ - cluster: string; - /** Total number of nodes */ - 'node.total': number; - /** Number of nodes than can store data */ - 'node.data': number; - /** Number of shards */ - shards: number; - /** Number of primary shards */ - pri: number; - /** Number of relocating nodes */ - relo: number; - /** Number of initializing nodes */ - init: number; - /** Number of unassigned shards */ - unassign: number; - /** Number of pending tasks */ - pending_tasks: number; - /** Wait time of longest task pending */ - max_task_wait_time: number; - /** Active number of shards in percent */ - active_shards_percent: string; -}[]; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +/** Status */ +export type Status = { + /** Timestamp */ + epoch: number; + /** Timestamp */ + timestamp: string; + /** Health status */ + status: 'red' | 'yellow' | 'green'; + /** Cluster name */ + cluster: string; + /** Total number of nodes */ + 'node.total': number; + /** Number of nodes than can store data */ + 'node.data': number; + /** Number of shards */ + shards: number; + /** Number of primary shards */ + pri: number; + /** Number of relocating nodes */ + relo: number; + /** Number of initializing nodes */ + init: number; + /** Number of unassigned shards */ + unassign: number; + /** Number of pending tasks */ + pending_tasks: number; + /** Wait time of longest task pending */ + max_task_wait_time: number; + /** Active number of shards in percent */ + active_shards_percent: string; +}[]; + diff --git a/packages/providers/http-client/README.md b/packages/providers/http-client/README.md index 384df471..fb6cd815 100644 --- a/packages/providers/http-client/README.md +++ b/packages/providers/http-client/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,8 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Health checks**](#health-checks) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** @@ -53,12 +56,87 @@ Check information about **@mdf.js** providers in the documentation of the core m ## **Use** +The provider implemented in this module wraps the [axios](https://www.npmjs.com/package/axios) client. + +```typescript +import { HTTP } from '@mdf.js/http-client-provider'; + +const client = HTTP.Factory.create({ + name: `myHTTPClientProvider`, + config: { + requestConfig?: {...}, // a CreateAxiosDefaults object from axios + httpAgentOptions: {...}, // an http AgentOptions object from Node.js + httpsAgentOptions: {...}, // an https AgentOptions object from Node.js + }, + logger: myLoggerInstance, + useEnvironment: true, +}); +``` + +- **Defaults**: + + ```typescript + {} + ``` + +- **Environment**: remember to set the `useEnvironment` flag to `true` to use these environment variables. + + ```typescript + { + requestConfig: { + baseURL: process.env['CONFIG_HTTP_CLIENT_BASE_URL'], + timeout: process.env['CONFIG_HTTP_CLIENT_TIMEOUT'], + auth: { // Only if username and password are set + username: process.env['CONFIG_HTTP_CLIENT_AUTH_USERNAME'], + password: process.env['CONFIG_HTTP_CLIENT_AUTH_PASSWORD'], + }, + }, + httpAgentOptions: { + keepAlive: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE'], + keepAliveInitialDelay: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY'], + keepAliveMsecs: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS'], + maxSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS'], + maxTotalSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL'], + maxFreeSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE'], + }, + httpsAgentOptions: { + keepAlive: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE'], + keepAliveInitialDelay: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_INITIAL_DELAY'], + keepAliveMsecs: process.env['CONFIG_HTTP_CLIENT_KEEPALIVE_MSECS'], + maxSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS'], + maxTotalSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_TOTAL'], + maxFreeSockets: process.env['CONFIG_HTTP_CLIENT_MAX_SOCKETS_FREE'], + rejectUnauthorized: process.env['CONFIG_HTTP_CLIENT_REJECT_UNAUTHORIZED'], + ca: process.env['CONFIG_HTTP_CLIENT_CA_PATH'], + cert: process.env['CONFIG_HTTP_CLIENT_CLIENT_CERT_PATH'], + key: process.env['CONFIG_HTTP_CLIENT_CLIENT_KEY_PATH'], + }, + } + ``` + +### **Health checks** + Checks included in the provider: - **status**: Due to the nature of the HTTP client, the status check is not implemented. The provider is always in `running` state. - **observedValue**: `running`. - **status**: `pass`. - **output**: `undefined`. + - **componentType**: `service`. + +```json +{ + "[mdf-http-client:status]": [ + { + "status": "pass", + "componentId": "00000000-0000-0000-0000-000000000000", + "observedValue": "running", + "componentType": "service", + "output": undefined, + }, + ], +} +``` ## **Environment variables** diff --git a/packages/providers/http-client/package.json b/packages/providers/http-client/package.json index ec5616b5..575b9cc7 100644 --- a/packages/providers/http-client/package.json +++ b/packages/providers/http-client/package.json @@ -1,52 +1,52 @@ -{ - "name": "@mdf.js/http-client-provider", - "version": "0.0.1", - "description": "MMS - HTTP-S Client Port for Javascript/Typescript", - "keywords": [ - "NodeJS", - "provider", - "MMS", - "http-client", - "express" - ], - "repository": { - "type": "git", - "url": "http-clients://devopmytra.visualstudio.com/MytraManagementSystem/_git/MMS-microservices-nx", - "directory": "packages/providers/http-client" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/http-client/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "axios": "^1.7.7", - "joi": "^17.13.3", - "tslib": "^2.7.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/debug": "^4.1.8" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/http-client-provider", + "version": "0.0.1", + "description": "MMS - HTTP-S Client Port for Javascript/Typescript", + "keywords": [ + "NodeJS", + "provider", + "MMS", + "http-client", + "express" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/providers/http-client" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/http-client/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "axios": "^1.7.9", + "joi": "^17.13.3", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/debug": "^4.1.8" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/providers/http-client/src/config/schema.ts b/packages/providers/http-client/src/config/schema.ts index 02adefc3..8b308933 100644 --- a/packages/providers/http-client/src/config/schema.ts +++ b/packages/providers/http-client/src/config/schema.ts @@ -1,39 +1,40 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import Joi from 'joi'; - -export const schema = Joi.object({ - requestConfig: Joi.object({ - baseURL: Joi.string().uri(), - timeout: Joi.number().integer().min(0), - auth: Joi.object({ - username: Joi.string(), - password: Joi.string(), - }), - }), - httpAgentOptions: Joi.object({ - keepAlive: Joi.boolean(), - keepAliveInitialDelay: Joi.number().integer().min(0), - keepAliveMsecs: Joi.number().integer().min(0), - maxSockets: Joi.number().integer().min(0), - maxTotalSockets: Joi.number().integer().min(0), - maxFreeSockets: Joi.number().integer().min(0), - }), - httpsAgentOptions: Joi.object({ - keepAlive: Joi.boolean(), - keepAliveInitialDelay: Joi.number().integer().min(0), - keepAliveMsecs: Joi.number().integer().min(0), - maxSockets: Joi.number().integer().min(0), - maxTotalSockets: Joi.number().integer().min(0), - maxFreeSockets: Joi.number().integer().min(0), - rejectUnauthorized: Joi.boolean(), - ca: Joi.string(), - cert: Joi.string(), - key: Joi.string(), - }), -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import Joi from 'joi'; + +export const schema = Joi.object({ + requestConfig: Joi.object({ + baseURL: Joi.string().uri(), + timeout: Joi.number().integer().min(0), + auth: Joi.object({ + username: Joi.string(), + password: Joi.string(), + }), + }).unknown(true), + httpAgentOptions: Joi.object({ + keepAlive: Joi.boolean(), + keepAliveInitialDelay: Joi.number().integer().min(0), + keepAliveMsecs: Joi.number().integer().min(0), + maxSockets: Joi.number().integer().min(0), + maxTotalSockets: Joi.number().integer().min(0), + maxFreeSockets: Joi.number().integer().min(0), + }).unknown(true), + httpsAgentOptions: Joi.object({ + keepAlive: Joi.boolean(), + keepAliveInitialDelay: Joi.number().integer().min(0), + keepAliveMsecs: Joi.number().integer().min(0), + maxSockets: Joi.number().integer().min(0), + maxTotalSockets: Joi.number().integer().min(0), + maxFreeSockets: Joi.number().integer().min(0), + rejectUnauthorized: Joi.boolean(), + ca: Joi.string(), + cert: Joi.string(), + key: Joi.string(), + }).unknown(true), +}).unknown(true); + diff --git a/packages/providers/http-client/src/config/utils.ts b/packages/providers/http-client/src/config/utils.ts index cde3fc08..7602323a 100644 --- a/packages/providers/http-client/src/config/utils.ts +++ b/packages/providers/http-client/src/config/utils.ts @@ -1,52 +1,50 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { DebugLogger } from '@mdf.js/logger'; -/** Base name for the configuration provider */ -export const CONFIG_PROVIDER_BASE_NAME = 'http-client'; -/** - * Artifact identifier for the configuration provider - * @defaultValue `mdf-http-client` - */ -export const CONFIG_ARTIFACT_ID = `mdf-${CONFIG_PROVIDER_BASE_NAME}`; -/** Default Logger for the configuration provider */ -export const logger = new DebugLogger(`mdf:${CONFIG_PROVIDER_BASE_NAME}:config`); - -/** - * Return the configuration object if any of the properties is not undefined - * @param config - configuration object - * @returns - */ -export function checkConfigObject(config: T): T | undefined { - if ( - config && - typeof config === 'object' && - typeof config !== null && - typeof config !== 'function' && - !Array.isArray(config) - ) { - return Object.values(config).some(value => value !== undefined) ? config : undefined; - } else { - return undefined; - } -} - -/** - * Return username and password if both are provided - * @param username - username environment variable - * @param password - password environment variable - * @returns - */ -export function selectAuth( - username?: string, - password?: string -): { username: string; password: string } | undefined { - if (username && password) { - return { username, password }; - } else { - return undefined; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { DebugLogger } from '@mdf.js/logger'; +/** Base name for the configuration provider */ +export const CONFIG_PROVIDER_BASE_NAME = 'http-client'; +/** + * Artifact identifier for the configuration provider + * @defaultValue `mdf-http-client` + */ +export const CONFIG_ARTIFACT_ID = `mdf-${CONFIG_PROVIDER_BASE_NAME}`; +/** Default Logger for the configuration provider */ +export const logger = new DebugLogger(`mdf:${CONFIG_PROVIDER_BASE_NAME}:config`); + +/** + * Return the configuration object if any of the properties is not undefined + * @param config - configuration object + * @returns + */ +export function checkConfigObject(config: T): T | undefined { + if ( + config && + typeof config === 'object' && + typeof config !== 'function' && + !Array.isArray(config) + ) { + return Object.values(config).some(value => value !== undefined) ? config : undefined; + } + return undefined; +} + +/** + * Return username and password if both are provided + * @param username - username environment variable + * @param password - password environment variable + * @returns + */ +export function selectAuth( + username?: string, + password?: string +): { username: string; password: string } | undefined { + if (username && password) { + return { username, password }; + } else { + return undefined; + } +} diff --git a/packages/providers/http-client/src/provider/index.ts b/packages/providers/http-client/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/http-client/src/provider/index.ts +++ b/packages/providers/http-client/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/http-server/README.md b/packages/providers/http-server/README.md index ee06bea9..c4981244 100644 --- a/packages/providers/http-server/README.md +++ b/packages/providers/http-server/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,8 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Health checks**](#health-checks) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** @@ -53,12 +56,68 @@ Check information about **@mdf.js** providers in the documentation of the core m ## **Use** +The provider implemented in this module wraps an HTTP server based on [express](https://www.npmjs.com/package/express) to provide a simple HTTP server. + +```typescript +import { HTTP } from '@mdf.js/http-server-provider'; + +const client = HTTP.Factory.create({ + name: `myHTTPServerProvider`, + config: { + /** Port for the HTTP server */ + port: 8080, + /** Host for the HTTP server */ + host: 'localhost', + /** Express app configuration */ + app: Express, + }, + logger: myLoggerInstance, + useEnvironment: true, +}); +``` + +- **Defaults**: + + ```typescript + { + port: 8080, + host: 'localhost', + app: /* Default static web */, + } + ``` + +- **Environment**: remember to set the `useEnvironment` flag to `true` to use these environment variables. + + ```typescript + { + port: process.env['CONFIG_SERVER_PORT'], + host: process.env['CONFIG_SERVER_HOST'], + } + ``` + +### **Health checks** + Checks included in the provider: - **status**: Due to the nature of the HTTP server, the status could be `running` if the server has been started properly, `stopped` if the server has been stopped or is not initialized, or `error` if the server could not be started. - **observedValue**: `running` if the server is running, `stopped` if the server is stopped, or `error` if the server could not be started. - **status**: `pass` if the server is running, `fail` could not be started or `warn` if the server is stopped. - **output**: In case of `error` state (status `fail`), the error message is shown. + - **componentType**: `service`. + +```json +{ + "[mdf-http-server:status]": [ + { + "status": "pass", + "componentId": "00000000-0000-0000-0000-000000000000", + "observedValue": "running", + "componentType": "service", + "output": undefined, + }, + ], +} +``` ## **Environment variables** diff --git a/packages/providers/http-server/package.json b/packages/providers/http-server/package.json index 356df8bc..bba029f2 100644 --- a/packages/providers/http-server/package.json +++ b/packages/providers/http-server/package.json @@ -11,7 +11,7 @@ ], "repository": { "type": "git", - "url": "http-servers://devopmytra.visualstudio.com/MytraManagementSystem/_git/MMS-microservices-nx", + "url": "https://github.com/mytracontrol/mdf.js.git", "directory": "packages/providers/http-server" }, "license": "MIT", @@ -35,10 +35,10 @@ "@mdf.js/crash": "*", "@mdf.js/logger": "*", "@mdf.js/utils": "*", - "express": "^4.21.1", + "express": "^4.21.2", "http-terminator": "^3.2.0", "joi": "^17.13.3", - "tslib": "^2.7.0" + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*", diff --git a/packages/providers/http-server/src/provider/index.ts b/packages/providers/http-server/src/provider/index.ts index 21a1e9f3..38e05d6f 100644 --- a/packages/providers/http-server/src/provider/index.ts +++ b/packages/providers/http-server/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Config, ProviderInstance as Provider, Server } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Config, ProviderInstance as Provider, Server } from './types'; + diff --git a/packages/providers/http-server/src/provider/types/Config.t.ts b/packages/providers/http-server/src/provider/types/Config.t.ts index 4b1eff7d..240cf171 100644 --- a/packages/providers/http-server/src/provider/types/Config.t.ts +++ b/packages/providers/http-server/src/provider/types/Config.t.ts @@ -1,12 +1,18 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Express } from 'express'; -export interface Config { - port?: number; - host?: string; - app?: Express; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Express } from 'express'; + +/** Configuration for the HTTP server provider */ +export interface Config { + /** Port to listen on */ + port?: number; + /** Host to listen on */ + host?: string; + /** Express app to use */ + app?: Express; +} + diff --git a/packages/providers/jsonl-archiver/README.md b/packages/providers/jsonl-archiver/README.md index f8a2ec3e..5deb797b 100644 --- a/packages/providers/jsonl-archiver/README.md +++ b/packages/providers/jsonl-archiver/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/providers/jsonl-archiver/package.json b/packages/providers/jsonl-archiver/package.json index fc724df1..c4b7e5c7 100644 --- a/packages/providers/jsonl-archiver/package.json +++ b/packages/providers/jsonl-archiver/package.json @@ -33,15 +33,15 @@ "@mdf.js/core": "*", "@mdf.js/crash": "*", "@mdf.js/logger": "*", - "@mdf.js/utils": "*", "@mdf.js/tasks": "*", - "uuid": "^10.0.0", - "lodash": "^4.17.21" + "@mdf.js/utils": "*", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "uuid": "^11.0.3" }, "devDependencies": { "@types/debug": "^4.1.8", - "@types/lodash": "^4.17.7", - "@types/uuid": "^10.0.0" + "@types/lodash": "^4.17.13" }, "engines": { "node": ">=16.14.2" diff --git a/packages/providers/jsonl-archiver/src/Client/ArchiverManager.ts b/packages/providers/jsonl-archiver/src/Client/ArchiverManager.ts index 72a95d4b..e7b4ec2b 100644 --- a/packages/providers/jsonl-archiver/src/Client/ArchiverManager.ts +++ b/packages/providers/jsonl-archiver/src/Client/ArchiverManager.ts @@ -21,6 +21,7 @@ import { FileStats, } from './types'; +/** Class responsible of managing jsonl file store operations */ export declare interface ArchiverManager { /** * Add a listener for the `error` event, emitted when there is an error in a file handler @@ -120,7 +121,6 @@ export class ArchiverManager extends EventEmitter { /** * Appends data to a JSONL file. * @param data - Data to append - * @param filename - Name of the file to append data to */ public async append(data: Record | Record[]): Promise; /** diff --git a/packages/providers/jsonl-archiver/src/Client/FileHandler.ts b/packages/providers/jsonl-archiver/src/Client/FileHandler.ts index 8eb2ca1a..8412953f 100644 --- a/packages/providers/jsonl-archiver/src/Client/FileHandler.ts +++ b/packages/providers/jsonl-archiver/src/Client/FileHandler.ts @@ -39,6 +39,7 @@ export declare interface FileHandler { */ on(event: 'resolve', listener: (stats: FileStats) => void): this; } + /** File handler class for managing file operations */ export class FileHandler extends EventEmitter { /** The name of the current file being managed */ diff --git a/packages/providers/jsonl-archiver/src/Client/types/ArchiveOptions.i.ts b/packages/providers/jsonl-archiver/src/Client/types/ArchiveOptions.i.ts index acccc6f2..e9707220 100644 --- a/packages/providers/jsonl-archiver/src/Client/types/ArchiveOptions.i.ts +++ b/packages/providers/jsonl-archiver/src/Client/types/ArchiveOptions.i.ts @@ -13,28 +13,28 @@ export interface ArchiveOptions { /** * Separator to use when writing the data to the file * @example '\n' - * @default '\n' + * @defaultValue '\n' */ separator?: string; /** * If set, this property will be used to store the data in the file, it could be a nested property * in the data object expressed as a dot separated string * @example 'data.property' - * @default undefined + * @defaultValue undefined */ propertyData?: string; /** * If set, this property will be used as the filename, it could be a nested property in the data * object expressed as a dot separated string * @example 'data.property' - * @default undefined + * @defaultValue undefined */ propertyFileName?: string; /** * If set, this property will be used to skip the data, it could be a nested property in the data * object expressed as a dot separated string * @example 'data.property' - * @default undefined + * @defaultValue undefined */ propertySkip?: string; /** @@ -42,37 +42,37 @@ export interface ArchiveOptions { * value is not set, but `propertySkip` is set, a not falsy value will be used to skip the data, * this means that any value that is not `false`, `0` or `''` will be used to skip the data. * @example 'skip' | 0 | false - * @default undefined + * @defaultValue undefined */ propertySkipValue?: string | number | boolean; /** * Base filename for the files * @example 'file' - * @default 'file' + * @defaultValue 'file' */ defaultBaseFilename?: string; /** * Path to the folder where the working files are stored * @example './data/working' - * @default './data/working' + * @defaultValue './data/working' */ workingFolderPath: string; /** * Path to the folder where the closed files are stored * @example './data/archive' - * @default './data/archive' + * @defaultValue './data/archive' */ archiveFolderPath: string; /** * If true, it will create the folders if they don't exist * @example true - * @default true + * @defaultValue true */ createFolders: boolean; /** * Maximum inactivity time in milliseconds before a handler is cleaned up * @example 60000 - * @default undefined + * @defaultValue undefined */ inactiveTimeout?: number; /** @@ -83,30 +83,30 @@ export interface ArchiveOptions { /** * Interval in milliseconds to rotate the file * @example 3600000 (1 hour) - * @default undefined + * @defaultValue undefined */ rotationInterval?: number; /** * Max size of the file before rotating it * @example 10485760 (10 MB) - * @default undefined + * @defaultValue undefined */ rotationSize?: number; /** * Max number of lines before rotating the file * @example 10000 (10k lines) - * @default undefined + * @defaultValue undefined */ rotationLines?: number; /** * Retry options for the file handler operations * @example { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 } - * @default { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 } + * @defaultValue { attempts: 3, timeout: 1000, waitTime: 1000, maxWaitTime: 10000 } */ retryOptions?: RetryOptions; /** * Logger instance to use - * @default undefined + * @defaultValue undefined */ logger?: LoggerInstance; } diff --git a/packages/providers/jsonl-archiver/src/index.ts b/packages/providers/jsonl-archiver/src/index.ts index 267ac1ef..cd0f8147 100644 --- a/packages/providers/jsonl-archiver/src/index.ts +++ b/packages/providers/jsonl-archiver/src/index.ts @@ -5,5 +5,5 @@ * or at https://opensource.org/licenses/MIT. */ -export { ArchiveOptions, ArchiverManager, FileStats } from './Client'; +export { AppendResult, ArchiveOptions, ArchiverManager, FileStats } from './Client'; export * as JSONLArchiver from './provider'; diff --git a/packages/providers/jsonl-archiver/src/provider/index.ts b/packages/providers/jsonl-archiver/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/jsonl-archiver/src/provider/index.ts +++ b/packages/providers/jsonl-archiver/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/kafka/README.md b/packages/providers/kafka/README.md index c25bec22..dd044c92 100644 --- a/packages/providers/kafka/README.md +++ b/packages/providers/kafka/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -100,7 +101,6 @@ Checks included in the provider: - **CONFIG\_KAFKA\_LOG\_LEVEL** (default: `` `error` ``): Define the log level for the kafka provider, possible values are: - \`error\` - \`warn\` - \`info\` - \`debug\` - \`trace\` - **CONFIG\_KAFKA\_CLIENT\_\_CLIENT\_ID** (default: `hostname`): Client identifier - **CONFIG\_KAFKA\_CLIENT\_\_BROKERS** (default: `'127.0.0.1:9092'`): Kafka brokers -- **CONFIG\_KAFKA\_CLIENT\_\_BROKERS** (default: `'127.0.0.1:9092'`): Kafka brokers - **CONFIG\_KAFKA\_CLIENT\_\_CONNECTION\_TIMEOUT** (default: `1000`): Time in milliseconds to wait for a successful connection - **CONFIG\_KAFKA\_CLIENT\_\_AUTHENTICATION\_TIMEOUT** (default: `1000`): Timeout in ms for authentication requests - **CONFIG\_KAFKA\_CLIENT\_\_REAUTHENTICATION\_THRESHOLD** (default: `1000`): When periodic reauthentication (connections.max.reauth.ms) is configured on the broker side, reauthenticate when \`reauthenticationThreshold\` milliseconds remain of session lifetime. @@ -119,7 +119,7 @@ Checks included in the provider: - **CONFIG\_KAFKA\_CLIENT\_SSL\_KEY\_PATH** (default: `undefined`): Path to the client key. - **CONFIG\_KAFKA\_CLIENT\_\_SASL\_USERNAME** (default: `undefined`): SASL username - **CONFIG\_KAFKA\_CLIENT\_\_SASL\_PASSWORD** (default: `undefined`): SASL password -- **NODE\_APP\_INSTANCE**: undefined +- **NODE\_APP\_INSTANCE** (default: `undefined`): Used as default container id, receiver name, sender name, etc. in cluster configurations. ## **License** diff --git a/packages/providers/kafka/package.json b/packages/providers/kafka/package.json index a386f467..83a9da5c 100644 --- a/packages/providers/kafka/package.json +++ b/packages/providers/kafka/package.json @@ -1,54 +1,53 @@ -{ - "name": "@mdf.js/kafka-provider", - "version": "0.0.1", - "description": "MMS - Kafka Port for Javascript/Typescript", - "keywords": [ - "NodeJS", - "provider", - "MMS", - "kafka" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/providers/kafka" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/elastic/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "joi": "^17.13.3", - "kafkajs": "^2.2.4", - "lodash": "^4.17.21", - "tslib": "^2.7.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/kafka-provider", + "version": "0.0.1", + "description": "MMS - Kafka Port for Javascript/Typescript", + "keywords": [ + "NodeJS", + "provider", + "MMS", + "kafka" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/providers/kafka" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/elastic/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "joi": "^17.13.3", + "kafkajs": "^2.2.4", + "lodash": "^4.17.21", + "tslib": "^2.8.1", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/providers/kafka/src/Client/Client.ts b/packages/providers/kafka/src/Client/Client.ts index 79083dcb..b978ee34 100644 --- a/packages/providers/kafka/src/Client/Client.ts +++ b/packages/providers/kafka/src/Client/Client.ts @@ -1,230 +1,230 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Crash } from '@mdf.js/crash'; -import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; -import { EventEmitter } from 'events'; -import { - Admin, - GroupDescription, - ITopicMetadata, - InstrumentationEvent, - Kafka, - KafkaConfig, - LogEntry, - logCreator, -} from 'kafkajs'; -import { inspect } from 'util'; -import { v4 } from 'uuid'; - -export { KafkaConfig as KafkaClientOptions, logLevel } from 'kafkajs'; - -const DEFAULT_CHECK_INTERVAL = 30000; - -export type SystemStatus = { topics: ITopicMetadata[]; groups: GroupDescription[] }; - -export declare interface Client { - /** Emitted when admin client can collect the desired information */ - on(event: 'healthy', listener: () => void): this; - /** Emitted when admin client can not collect the desired information */ - on(event: 'unhealthy', listener: (crash: Crash) => void): this; - /** Emitted every time that admin client get metadata from brokers */ - on(event: 'status', listener: (status?: SystemStatus) => void): this; - /** Emitted when admin client has some problem getting the metadata from brokers */ - on(event: 'error', listener: (crash: Crash) => void): this; -} - -/** - * This client is based in the functionality of [kafkajs](https://kafka.js.org) for NodeJS. - * In the moment of implement the port, this library show some issues and not clear API about how - * the connection handshake and events are managed by the library, for this reason we decide to - * include an admin client in all the cases, even when we only want to stablish a consumer or - * producer. - */ -export abstract class Client extends EventEmitter { - /** Debug logger for development and deep troubleshooting */ - protected readonly logger: LoggerInstance; - /** Instance identification */ - protected readonly componentId = v4(); - /** Kafka Broker configuration options */ - readonly options: KafkaConfig; - /** Kafka Client */ - protected readonly instance: Kafka; - /** Kafka admin instance */ - private readonly admin: Admin; - /** Check interval */ - private timeInterval?: NodeJS.Timeout; - /** System status */ - private status: SystemStatus | undefined = undefined; - /** Connection state flag */ - protected connected: boolean; - /** Healthy flag, resumed state of brokers system */ - protected healthy: boolean; - /** First check flag */ - private isFirstCheck = true; - /** - * Create an instance of a Kafka client configuration options - * @param options - Kafka client configuration options - * @param interval - Period of health check interval - */ - constructor( - options: KafkaConfig, - private readonly interval = DEFAULT_CHECK_INTERVAL - ) { - super(); - this.logger = SetContext(new DebugLogger('mdf:client:kafka'), 'kafka', this.componentId); - // Stryker disable next-line all - this.logger.debug(`New instance of Kafka Client created: ${this.componentId}`); - this.options = { ...options, logCreator: options.logCreator ?? this.defaultLogCreator }; - this.interval = Math.floor((this.options.requestTimeout || interval) * 1.1); - this.instance = new Kafka(this.options); - this.admin = this.instance.admin(); - this.connected = false; - this.healthy = false; - } - /** Overall client state */ - public get state(): boolean { - return this.connected && this.healthy; - } - /** - * Log creator function, used to log kafka events - * @param level - configured log level - * @returns - */ - private readonly defaultLogCreator: logCreator = () => (entry: LogEntry) => { - const { logger, message, ...others } = entry.log; - const logMessage = `${logger} - ${entry.label} - ${entry.namespace} - ${message}`; - this.logger.debug(logMessage); - if (others) { - this.logger.silly(inspect(others, false, 6)); - } - }; - /** Perform the connection of the instance to the system */ - protected async start(): Promise { - if (this.connected) { - return; - } - try { - await this.admin.connect(); - for (const event of Object.values(this.admin.events)) { - this.admin.on(event, this.eventLogging); - } - this.connected = true; - if (!this.timeInterval) { - this.timeInterval = setInterval(this.checkHealth, this.interval); - await this.checkHealth(); - } - } catch (error) { - const cause = Crash.from(error, this.componentId); - throw new Crash(`Error setting the monitoring client: ${cause.message}`, this.componentId, { - cause, - }); - } - } - /** Perform the disconnection of the instance from the system */ - protected async stop(): Promise { - if (!this.connected) { - return; - } - try { - if (this.timeInterval) { - clearInterval(this.timeInterval); - this.timeInterval = undefined; - } - await this.admin.disconnect(); - this.connected = false; - } catch (error) { - const cause = Crash.from(error, this.componentId); - throw new Crash( - `Error in disconnection process of monitor client: ${cause.message}`, - this.componentId, - { cause } - ); - } - } - /** Check the state of the topics over the brokers */ - private readonly checkHealth = async (): Promise => { - // Stryker disable next-line all - this.logger.debug(`Checking the state of the topics...`); - this.status = { topics: [], groups: [] }; - try { - const fetchedTopics = await this.admin.fetchTopicMetadata(); - this.status.topics = fetchedTopics.topics.filter(entry => !entry.name.startsWith('__')); - this.status.groups = []; - const fetchedGroups = await this.admin.listGroups(); - if (fetchedGroups && fetchedGroups.groups.length) { - const descriptions = await this.admin.describeGroups( - fetchedGroups.groups.map(entry => entry.groupId) - ); - if (descriptions) { - this.status.groups = descriptions.groups.map(group => { - //@ts-ignore Buffer kind information not give us any real understanding - group.members = group.members.map(member => { - return { - memberId: member.memberId, - clientId: member.clientId, - clientHost: member.clientHost, - }; - }); - return group; - }); - } - } - this.emit('status', this.status); - if (!this.isFirstCheck && !this.healthy) { - this.healthy = true; - this.emit('healthy'); - } - } catch (error) { - this.status = undefined; - this.emit('status', this.status); - const cause = Crash.from(error, this.componentId); - const crash = new Crash(`Error checking the system: ${cause.message}`, this.componentId, { - cause, - }); - this.logger.error(crash.message); - if (this.isFirstCheck) { - this.emit('error', crash); - // The first time we want only transmit the error, not the healthy event, but the next - // times we want to transmit the healthy event if the system is not healthy - this.healthy = true; - } else if (this.healthy) { - this.healthy = false; - this.emit('unhealthy', crash); - } - } finally { - // Stryker disable next-line all - this.logger.silly(`STATUS: ${JSON.stringify(this.status, null, 2)}`); - this.isFirstCheck = false; - } - }; - /** - * Log an event using the DEBUG logger for troubleshooting - * @param context - event context - */ - protected eventLogging = (context: InstrumentationEvent): void => { - const { type, timestamp, payload, id } = context; - const date = new Date(timestamp).toISOString(); - // Stryker disable next-line all - this.logger.debug(`[${type}] event in client with [${id}] at [${date}]`); - if (payload) { - // Stryker disable next-line all - this.logger.silly(inspect(payload, false, 6)); - } - }; - /** - * Always retry to perform the process on failure function - * @param cause - source of the failure - */ - protected onFailure = async (cause: Error): Promise => { - const error = Crash.from(cause, this.componentId); - // Stryker disable next-line all - this.logger.error(`Retry failure for [${error.name}] ${error.message}`); - this.emit('error', error); - return true; - }; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Crash } from '@mdf.js/crash'; +import { DebugLogger, LoggerInstance, SetContext } from '@mdf.js/logger'; +import { EventEmitter } from 'events'; +import { + Admin, + GroupDescription, + ITopicMetadata, + InstrumentationEvent, + Kafka, + KafkaConfig, + LogEntry, + logCreator, +} from 'kafkajs'; +import { inspect } from 'util'; +import { v4 } from 'uuid'; + +export { KafkaConfig as KafkaClientOptions, logLevel } from 'kafkajs'; + +const DEFAULT_CHECK_INTERVAL = 30000; + +export type SystemStatus = { topics: ITopicMetadata[]; groups: GroupDescription[] }; + +export declare interface Client { + /** Emitted when admin client can collect the desired information */ + on(event: 'healthy', listener: () => void): this; + /** Emitted when admin client can not collect the desired information */ + on(event: 'unhealthy', listener: (crash: Crash) => void): this; + /** Emitted every time that admin client get metadata from brokers */ + on(event: 'status', listener: (status?: SystemStatus) => void): this; + /** Emitted when admin client has some problem getting the metadata from brokers */ + on(event: 'error', listener: (crash: Crash) => void): this; +} + +/** + * This client is based in the functionality of [kafkajs](https://kafka.js.org) for NodeJS. + * In the moment of implement the port, this library show some issues and not clear API about how + * the connection handshake and events are managed by the library, for this reason we decide to + * include an admin client in all the cases, even when we only want to stablish a consumer or + * producer. + */ +export abstract class Client extends EventEmitter { + /** Debug logger for development and deep troubleshooting */ + protected readonly logger: LoggerInstance; + /** Instance identification */ + protected readonly componentId = v4(); + /** Kafka Broker configuration options */ + readonly options: KafkaConfig; + /** Kafka Client */ + protected readonly instance: Kafka; + /** Kafka admin instance */ + private readonly admin: Admin; + /** Check interval */ + private timeInterval?: NodeJS.Timeout; + /** System status */ + private status: SystemStatus | undefined = undefined; + /** Connection state flag */ + protected connected: boolean; + /** Healthy flag, resumed state of brokers system */ + protected healthy: boolean; + /** First check flag */ + private isFirstCheck = true; + /** + * Create an instance of a Kafka client configuration options + * @param options - Kafka client configuration options + * @param interval - Period of health check interval + */ + constructor( + options: KafkaConfig, + private readonly interval = DEFAULT_CHECK_INTERVAL + ) { + super(); + this.logger = SetContext(new DebugLogger('mdf:client:kafka'), 'kafka', this.componentId); + // Stryker disable next-line all + this.logger.debug(`New instance of Kafka Client created: ${this.componentId}`); + this.options = { ...options, logCreator: options.logCreator ?? this.defaultLogCreator }; + this.interval = Math.floor((this.options.requestTimeout ?? interval) * 1.1); + this.instance = new Kafka(this.options); + this.admin = this.instance.admin(); + this.connected = false; + this.healthy = false; + } + /** Overall client state */ + public get state(): boolean { + return this.connected && this.healthy; + } + /** + * Log creator function, used to log kafka events + * @param level - configured log level + * @returns + */ + private readonly defaultLogCreator: logCreator = () => (entry: LogEntry) => { + const { logger, message, ...others } = entry.log; + const logMessage = `${logger} - ${entry.label} - ${entry.namespace} - ${message}`; + this.logger.debug(logMessage); + if (others) { + this.logger.silly(inspect(others, false, 6)); + } + }; + /** Perform the connection of the instance to the system */ + protected async start(): Promise { + if (this.connected) { + return; + } + try { + await this.admin.connect(); + for (const event of Object.values(this.admin.events)) { + this.admin.on(event, this.eventLogging); + } + this.connected = true; + if (!this.timeInterval) { + this.timeInterval = setInterval(this.checkHealth, this.interval); + await this.checkHealth(); + } + } catch (error) { + const cause = Crash.from(error, this.componentId); + throw new Crash(`Error setting the monitoring client: ${cause.message}`, this.componentId, { + cause, + }); + } + } + /** Perform the disconnection of the instance from the system */ + protected async stop(): Promise { + if (!this.connected) { + return; + } + try { + if (this.timeInterval) { + clearInterval(this.timeInterval); + this.timeInterval = undefined; + } + await this.admin.disconnect(); + this.connected = false; + } catch (error) { + const cause = Crash.from(error, this.componentId); + throw new Crash( + `Error in disconnection process of monitor client: ${cause.message}`, + this.componentId, + { cause } + ); + } + } + /** Check the state of the topics over the brokers */ + private readonly checkHealth = async (): Promise => { + // Stryker disable next-line all + this.logger.debug(`Checking the state of the topics...`); + this.status = { topics: [], groups: [] }; + try { + const fetchedTopics = await this.admin.fetchTopicMetadata(); + this.status.topics = fetchedTopics.topics.filter(entry => !entry.name.startsWith('__')); + this.status.groups = []; + const fetchedGroups = await this.admin.listGroups(); + if (fetchedGroups?.groups.length) { + const descriptions = await this.admin.describeGroups( + fetchedGroups.groups.map(entry => entry.groupId) + ); + if (descriptions) { + this.status.groups = descriptions.groups.map(group => { + //@ts-ignore Buffer kind information not give us any real understanding + group.members = group.members.map(member => { + return { + memberId: member.memberId, + clientId: member.clientId, + clientHost: member.clientHost, + }; + }); + return group; + }); + } + } + this.emit('status', this.status); + if (!this.isFirstCheck && !this.healthy) { + this.healthy = true; + this.emit('healthy'); + } + } catch (error) { + this.status = undefined; + this.emit('status', this.status); + const cause = Crash.from(error, this.componentId); + const crash = new Crash(`Error checking the system: ${cause.message}`, this.componentId, { + cause, + }); + this.logger.error(crash.message); + if (this.isFirstCheck) { + this.emit('error', crash); + // The first time we want only transmit the error, not the healthy event, but the next + // times we want to transmit the healthy event if the system is not healthy + this.healthy = true; + } else if (this.healthy) { + this.healthy = false; + this.emit('unhealthy', crash); + } + } finally { + // Stryker disable next-line all + this.logger.silly(`STATUS: ${JSON.stringify(this.status, null, 2)}`); + this.isFirstCheck = false; + } + }; + /** + * Log an event using the DEBUG logger for troubleshooting + * @param context - event context + */ + protected eventLogging = (context: InstrumentationEvent): void => { + const { type, timestamp, payload, id } = context; + const date = new Date(timestamp).toISOString(); + // Stryker disable next-line all + this.logger.debug(`[${type}] event in client with [${id}] at [${date}]`); + if (payload) { + // Stryker disable next-line all + this.logger.silly(inspect(payload, false, 6)); + } + }; + /** + * Always retry to perform the process on failure function + * @param cause - source of the failure + */ + protected onFailure = async (cause: Error): Promise => { + const error = Crash.from(cause, this.componentId); + // Stryker disable next-line all + this.logger.error(`Retry failure for [${error.name}] ${error.message}`); + this.emit('error', error); + return true; + }; +} diff --git a/packages/providers/kafka/src/Common/config/default.ts b/packages/providers/kafka/src/Common/config/default.ts index 69096326..7c484b92 100644 --- a/packages/providers/kafka/src/Common/config/default.ts +++ b/packages/providers/kafka/src/Common/config/default.ts @@ -11,9 +11,16 @@ import { CONFIG_KAFKA_CLIENT__LOG_LEVEL, defaultLogCreator } from './utils'; // ************************************************************************************************* // #region Default values + +/** + * Used as default container id, receiver name, sender name, etc. in cluster configurations. + * @defaultValue undefined + */ +export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; + const KAFKA_CLIENT__BROKERS = '127.0.0.1:9092'; const KAFKA_CLIENT__SSL = false; -const KAFKA_CLIENT__CLIENT_ID = process.env['NODE_APP_INSTANCE'] || hostname(); +const KAFKA_CLIENT__CLIENT_ID = NODE_APP_INSTANCE ?? hostname(); const KAFKA_CLIENT__CONNECTION_TIMEOUT = 1000; const KAFKA_CLIENT__REQUEST_TIMEOUT = 30000; const KAFKA_CLIENT__ENFORCE_REQUEST_TIMEOUT = false; @@ -46,4 +53,3 @@ export const defaultConfig: BaseConfig = { }, }; // #endregion - diff --git a/packages/providers/kafka/src/Common/config/env.ts b/packages/providers/kafka/src/Common/config/env.ts index f1ff79bb..8a3456d8 100644 --- a/packages/providers/kafka/src/Common/config/env.ts +++ b/packages/providers/kafka/src/Common/config/env.ts @@ -23,8 +23,9 @@ const CONFIG_KAFKA_CLIENT__CLIENT_ID = process.env['CONFIG_KAFKA_CLIENT__CLIENT_ * Kafka brokers * @defaultValue '127.0.0.1:9092' */ -const CONFIG_KAFKA_CLIENT__BROKERS = process.env['CONFIG_KAFKA_CLIENT__BROKERS'] - ? process.env['CONFIG_KAFKA_CLIENT__BROKERS'].split(',') +const _CONFIG_KAFKA_CLIENT__BROKERS = process.env['CONFIG_KAFKA_CLIENT__BROKERS']; +const CONFIG_KAFKA_CLIENT__BROKERS = _CONFIG_KAFKA_CLIENT__BROKERS + ? _CONFIG_KAFKA_CLIENT__BROKERS.split(',') : undefined; /** * Time in milliseconds to wait for a successful connection diff --git a/packages/providers/kafka/src/Common/config/utils.ts b/packages/providers/kafka/src/Common/config/utils.ts index 19dbf1f9..b14c2e2e 100644 --- a/packages/providers/kafka/src/Common/config/utils.ts +++ b/packages/providers/kafka/src/Common/config/utils.ts @@ -46,7 +46,7 @@ export type logCreator = (logLevel: logLevel) => (entry: LogEntry) => void; * @defaultValue `error` */ export const CONFIG_KAFKA_LOG_LEVEL: string | logLevel = - process.env['CONFIG_KAFKA_LOG_LEVEL'] || 'error'; + process.env['CONFIG_KAFKA_LOG_LEVEL'] ?? 'error'; const UUID = v4(); /** @@ -96,4 +96,3 @@ export const defaultLogCreator: logCreator = (level: logLevel) => (entry: LogEnt } }; // #endregion - diff --git a/packages/providers/kafka/src/Consumer/index.ts b/packages/providers/kafka/src/Consumer/index.ts index d0255314..006ade8e 100644 --- a/packages/providers/kafka/src/Consumer/index.ts +++ b/packages/providers/kafka/src/Consumer/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Config, Consumer, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Config, Consumer, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/kafka/src/Producer/index.ts b/packages/providers/kafka/src/Producer/index.ts index bafee24f..108f3859 100644 --- a/packages/providers/kafka/src/Producer/index.ts +++ b/packages/providers/kafka/src/Producer/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Config, Producer, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Config, Producer, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/mongo/README.md b/packages/providers/mongo/README.md index 53050c54..f6d2265a 100644 --- a/packages/providers/mongo/README.md +++ b/packages/providers/mongo/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,7 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** diff --git a/packages/providers/mongo/package.json b/packages/providers/mongo/package.json index 1c38eb22..b07de343 100644 --- a/packages/providers/mongo/package.json +++ b/packages/providers/mongo/package.json @@ -37,8 +37,8 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "joi": "^17.13.3", - "mongodb": "^6.8.0", - "tslib": "^2.7.0" + "mongodb": "^6.12.0", + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*" diff --git a/packages/providers/mongo/src/provider/Port.ts b/packages/providers/mongo/src/provider/Port.ts index f5b9ad5b..e4f5fb61 100644 --- a/packages/providers/mongo/src/provider/Port.ts +++ b/packages/providers/mongo/src/provider/Port.ts @@ -1,243 +1,244 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Layer } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { LoggerInstance } from '@mdf.js/logger'; -import { - CommandFailedEvent, - CommandSucceededEvent, - MongoClient, - ServerHeartbeatFailedEvent, - ServerHeartbeatSucceededEvent, -} from 'mongodb'; -import { inspect } from 'util'; -import { CONFIG_PROVIDER_BASE_NAME } from '../config'; -import { Client, Collections, Config, MONGO_CLIENT_EVENTS } from './types'; - -export class Port extends Layer.Provider.Port { - /** Mongo connection handler */ - private instance: Client; - /** Connection flag */ - private isConnected: boolean; - /** Mongo healthy state */ - private healthy: boolean; - /** Last failed command */ - private readonly lastFailedCommands: string[] = []; - /** - * Implementation of functionalities of a Mongo port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, config.appName || CONFIG_PROVIDER_BASE_NAME); - const cleanedOptions = { ...this.config, url: undefined, collections: undefined } as Config; - this.instance = new MongoClient(config.url as string, cleanedOptions); - // Stryker disable next-line all - this.logger.debug(`New instance of Mongo port created: ${this.uuid}`, this.uuid, this.name); - this.isConnected = false; - this.healthy = false; - } - /** Return the underlying port instance */ - public get client(): Client { - return this.instance; - } - /** Return the port state as a boolean value, true if the port is available, false in otherwise */ - public get state(): boolean { - return this.healthy; - } - /** Start the port, making it available */ - public async start(): Promise { - if (this.isConnected) { - // Stryker disable next-line all - this.logger.warn(`Port already started`, this.uuid, this.name); - return; - } - try { - await this.instance.connect(); - this.instance = this.eventsWrapping(this.instance); - this.isConnected = true; - if (this.config.collections) { - await this.createCollections(this.config.collections); - } - } catch (rawError) { - const error = Crash.from(rawError); - throw new Crash(`Error starting Mongo port: ${error.message}`, this.uuid, { cause: error }); - } - } - /** Stop the port, making it unavailable */ - public async stop(): Promise { - if (!this.isConnected) { - // Stryker disable next-line all - this.logger.warn(`Port already stopped`, this.uuid, this.name); - return; - } - try { - await this.instance.close(); - this.instance = this.eventsUnwrapping(this.instance); - this.isConnected = false; - } catch (rawError) { - const error = Crash.from(rawError); - throw new Crash(`Error stopping Mongo port: ${error.message}`, this.uuid, { cause: error }); - } - } - /** Close the port, alias to stop */ - public async close(): Promise { - await this.stop(); - } - /** - * Manage the event of a command failed - * @param event - event to be handled - */ - private readonly onCommandFailed = (event: CommandFailedEvent): void => { - const date = new Date(); - this.addCheck('lastCommand', { - componentId: this.uuid, - observedValue: 'failed', - observedUnit: 'command result', - status: 'fail', - output: `${event.commandName} - ${event.failure.message}`, - time: date.toISOString(), - }); - this.lastFailedCommands.push( - `${date.toISOString()} - ${event.commandName} - ${event.failure.message}` - ); - if (this.lastFailedCommands.length > 10) { - this.lastFailedCommands.shift(); - } - this.addCheck('lastFailedCommands', { - componentId: this.uuid, - observedValue: this.lastFailedCommands, - observedUnit: 'last failed commands', - status: 'pass', - output: undefined, - time: date.toISOString(), - }); - this.emit( - 'error', - new Crash(`${event.commandName} - ${event.failure.message}`, this.uuid, { - info: { date, event }, - }) - ); - }; - /** - * Manage the event of a command succeeded - * @param event - event to be handled - */ - private readonly onCommandSucceeded = (event: CommandSucceededEvent): void => { - this.addCheck('lastCommand', { - componentId: this.uuid, - observedValue: 'succeeded', - observedUnit: 'command result', - status: 'pass', - output: undefined, - time: new Date().toISOString(), - }); - }; - /** - * Manage the event of a heartbeat failed - * @param event - event to be handled - */ - private readonly onServerHeartbeatFailed = (event: ServerHeartbeatFailedEvent): void => { - this.addCheck('heartbeat', { - componentId: this.uuid, - observedValue: 'failed', - observedUnit: 'heartbeat result', - status: 'fail', - output: `${event.connectionId} - ${event.failure.message}`, - time: new Date().toISOString(), - }); - if (this.healthy) { - this.emit( - 'unhealthy', - new Crash(`Mongo port is unhealthy: ${event.failure.message}`, this.uuid) - ); - } - this.healthy = false; - }; - /** - * Manage the event of a server heartbeat succeeded - * @param event - event to be handled - */ - private readonly onServerHeartbeatSucceeded = (event: ServerHeartbeatSucceededEvent): void => { - this.addCheck('heartbeat', { - componentId: this.uuid, - observedValue: event, - observedUnit: 'heartbeat result', - status: 'pass', - output: undefined, - time: new Date().toISOString(), - }); - if (!this.healthy) { - this.emit('healthy'); - } - this.healthy = true; - }; - /** - * Wrap the events of the Mongo client - * @param event - event to be handled - * @returns - */ - private readonly onEvent = (event: string): ((meta: unknown) => void) => { - return (meta: unknown): void => { - // Stryker disable next-line all - this.logger.silly(`New incoming [${event}] event from Mongo with meta: ${inspect(meta)}`); - }; - }; - /** - * Attach all the events and log for debugging - * @param instance - client where the event managers will be attached - */ - private eventsWrapping(instance: MongoClient): MongoClient { - instance.on('commandSucceeded', this.onCommandSucceeded); - instance.on('commandFailed', this.onCommandFailed); - instance.on('serverHeartbeatSucceeded', this.onServerHeartbeatSucceeded); - instance.on('serverHeartbeatFailed', this.onServerHeartbeatFailed); - for (const event of MONGO_CLIENT_EVENTS) { - instance.on(event, this.onEvent(event)); - } - return instance; - } - /** - * Remove all the events listeners - * @param instance - client where the event managers will be unattached - */ - private eventsUnwrapping(instance: MongoClient): MongoClient { - instance.off('commandSucceeded', this.onCommandSucceeded); - instance.off('commandFailed', this.onCommandFailed); - instance.off('serverHeartbeatSucceeded', this.onServerHeartbeatSucceeded); - instance.off('serverHeartbeatFailed', this.onServerHeartbeatFailed); - for (const event of MONGO_CLIENT_EVENTS) { - instance.removeAllListeners(event); - } - return instance; - } - /** - * Check and create the collections indicated in the config - * @param collections - collections to be created - */ - private async createCollections(collections: Collections): Promise { - const actualCollections = ( - await this.client.db().listCollections({}, { nameOnly: true }).toArray() - ).map(collection => collection.name); - this.logger.debug(`Collections present in the database: [${actualCollections}]`); - for (const collection of Object.keys(collections)) { - if (!actualCollections.includes(collection)) { - this.logger.debug(`Creating collection: ${collection}`); - await this.client.db().createCollection(collection, collections[collection].options); - if (collections[collection].indexes && collections[collection].indexes.length > 0) { - this.logger.debug(`Creating indexes: ${inspect(collections[collection].indexes)}`); - await this.client - .db() - .collection(collection) - .createIndexes(collections[collection].indexes); - } - } else { - this.logger.debug(`Collection already exists: ${collection}`); - } - } - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { LoggerInstance } from '@mdf.js/logger'; +import { + CommandFailedEvent, + CommandSucceededEvent, + MongoClient, + ServerHeartbeatFailedEvent, + ServerHeartbeatSucceededEvent, +} from 'mongodb'; +import { inspect } from 'util'; +import { CONFIG_PROVIDER_BASE_NAME } from '../config'; +import { Client, Collections, Config, MONGO_CLIENT_EVENTS } from './types'; + +export class Port extends Layer.Provider.Port { + /** Mongo connection handler */ + private instance: Client; + /** Connection flag */ + private isConnected: boolean; + /** Mongo healthy state */ + private healthy: boolean; + /** Last failed command */ + private readonly lastFailedCommands: string[] = []; + /** + * Implementation of functionalities of a Mongo port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, config.appName ?? CONFIG_PROVIDER_BASE_NAME); + const cleanedOptions = { ...this.config, url: undefined, collections: undefined } as Config; + this.instance = new MongoClient(config.url as string, cleanedOptions); + // Stryker disable next-line all + this.logger.debug(`New instance of Mongo port created: ${this.uuid}`, this.uuid, this.name); + this.isConnected = false; + this.healthy = false; + } + /** Return the underlying port instance */ + public get client(): Client { + return this.instance; + } + /** Return the port state as a boolean value, true if the port is available, false in otherwise */ + public get state(): boolean { + return this.healthy; + } + /** Start the port, making it available */ + public async start(): Promise { + if (this.isConnected) { + // Stryker disable next-line all + this.logger.warn(`Port already started`, this.uuid, this.name); + return; + } + try { + await this.instance.connect(); + this.instance = this.eventsWrapping(this.instance); + this.isConnected = true; + if (this.config.collections) { + await this.createCollections(this.config.collections); + } + } catch (rawError) { + const error = Crash.from(rawError); + throw new Crash(`Error starting Mongo port: ${error.message}`, this.uuid, { cause: error }); + } + } + /** Stop the port, making it unavailable */ + public async stop(): Promise { + if (!this.isConnected) { + // Stryker disable next-line all + this.logger.warn(`Port already stopped`, this.uuid, this.name); + return; + } + try { + await this.instance.close(); + this.instance = this.eventsUnwrapping(this.instance); + this.isConnected = false; + } catch (rawError) { + const error = Crash.from(rawError); + throw new Crash(`Error stopping Mongo port: ${error.message}`, this.uuid, { cause: error }); + } + } + /** Close the port, alias to stop */ + public async close(): Promise { + await this.stop(); + } + /** + * Manage the event of a command failed + * @param event - event to be handled + */ + private readonly onCommandFailed = (event: CommandFailedEvent): void => { + const date = new Date(); + this.addCheck('lastCommand', { + componentId: this.uuid, + observedValue: 'failed', + observedUnit: 'command result', + status: 'fail', + output: `${event.commandName} - ${event.failure.message}`, + time: date.toISOString(), + }); + this.lastFailedCommands.push( + `${date.toISOString()} - ${event.commandName} - ${event.failure.message}` + ); + if (this.lastFailedCommands.length > 10) { + this.lastFailedCommands.shift(); + } + this.addCheck('lastFailedCommands', { + componentId: this.uuid, + observedValue: this.lastFailedCommands, + observedUnit: 'last failed commands', + status: 'pass', + output: undefined, + time: date.toISOString(), + }); + this.emit( + 'error', + new Crash(`${event.commandName} - ${event.failure.message}`, this.uuid, { + info: { date, event }, + }) + ); + }; + /** + * Manage the event of a command succeeded + * @param event - event to be handled + */ + private readonly onCommandSucceeded = (event: CommandSucceededEvent): void => { + this.addCheck('lastCommand', { + componentId: this.uuid, + observedValue: 'succeeded', + observedUnit: 'command result', + status: 'pass', + output: undefined, + time: new Date().toISOString(), + }); + }; + /** + * Manage the event of a heartbeat failed + * @param event - event to be handled + */ + private readonly onServerHeartbeatFailed = (event: ServerHeartbeatFailedEvent): void => { + this.addCheck('heartbeat', { + componentId: this.uuid, + observedValue: 'failed', + observedUnit: 'heartbeat result', + status: 'fail', + output: `${event.connectionId} - ${event.failure.message}`, + time: new Date().toISOString(), + }); + if (this.healthy) { + this.emit( + 'unhealthy', + new Crash(`Mongo port is unhealthy: ${event.failure.message}`, this.uuid) + ); + } + this.healthy = false; + }; + /** + * Manage the event of a server heartbeat succeeded + * @param event - event to be handled + */ + private readonly onServerHeartbeatSucceeded = (event: ServerHeartbeatSucceededEvent): void => { + this.addCheck('heartbeat', { + componentId: this.uuid, + observedValue: event, + observedUnit: 'heartbeat result', + status: 'pass', + output: undefined, + time: new Date().toISOString(), + }); + if (!this.healthy) { + this.emit('healthy'); + } + this.healthy = true; + }; + /** + * Wrap the events of the Mongo client + * @param event - event to be handled + * @returns + */ + private readonly onEvent = (event: string): ((meta: unknown) => void) => { + return (meta: unknown): void => { + // Stryker disable next-line all + this.logger.silly(`New incoming [${event}] event from Mongo with meta: ${inspect(meta)}`); + }; + }; + /** + * Attach all the events and log for debugging + * @param instance - client where the event managers will be attached + */ + private eventsWrapping(instance: MongoClient): MongoClient { + instance.on('commandSucceeded', this.onCommandSucceeded); + instance.on('commandFailed', this.onCommandFailed); + instance.on('serverHeartbeatSucceeded', this.onServerHeartbeatSucceeded); + instance.on('serverHeartbeatFailed', this.onServerHeartbeatFailed); + for (const event of MONGO_CLIENT_EVENTS) { + instance.on(event, this.onEvent(event)); + } + return instance; + } + /** + * Remove all the events listeners + * @param instance - client where the event managers will be unattached + */ + private eventsUnwrapping(instance: MongoClient): MongoClient { + instance.off('commandSucceeded', this.onCommandSucceeded); + instance.off('commandFailed', this.onCommandFailed); + instance.off('serverHeartbeatSucceeded', this.onServerHeartbeatSucceeded); + instance.off('serverHeartbeatFailed', this.onServerHeartbeatFailed); + for (const event of MONGO_CLIENT_EVENTS) { + instance.removeAllListeners(event); + } + return instance; + } + /** + * Check and create the collections indicated in the config + * @param collections - collections to be created + */ + private async createCollections(collections: Collections): Promise { + const actualCollections = ( + await this.client.db().listCollections({}, { nameOnly: true }).toArray() + ).map(collection => collection.name); + this.logger.debug(`Collections present in the database: [${actualCollections}]`); + for (const collection of Object.keys(collections)) { + if (!actualCollections.includes(collection)) { + this.logger.debug(`Creating collection: ${collection}`); + await this.client.db().createCollection(collection, collections[collection].options); + if (collections[collection].indexes && collections[collection].indexes.length > 0) { + this.logger.debug(`Creating indexes: ${inspect(collections[collection].indexes)}`); + await this.client + .db() + .collection(collection) + .createIndexes(collections[collection].indexes); + } + } else { + this.logger.debug(`Collection already exists: ${collection}`); + } + } + } +} + diff --git a/packages/providers/mongo/src/provider/index.ts b/packages/providers/mongo/src/provider/index.ts index 76d46432..8d43b6b9 100644 --- a/packages/providers/mongo/src/provider/index.ts +++ b/packages/providers/mongo/src/provider/index.ts @@ -1,10 +1,12 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Collection, CreateCollectionOptions, IndexDescription } from 'mongodb'; -export { Factory } from './Factory'; -export { Client, Collections, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Collection, CreateCollectionOptions, IndexDescription } from 'mongodb'; +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Collections, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/mqtt/README.md b/packages/providers/mqtt/README.md index 3d25e509..0f2d7a8c 100644 --- a/packages/providers/mqtt/README.md +++ b/packages/providers/mqtt/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,7 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** @@ -76,7 +78,7 @@ Checks included in the provider: - **CONFIG\_MQTT\_CLIENT\_CA\_PATH** (default: `undefined`): CA file path - **CONFIG\_MQTT\_CLIENT\_CLIENT\_CERT\_PATH** (default: `undefined`): Client cert file path - **CONFIG\_MQTT\_CLIENT\_CLIENT\_KEY\_PATH** (default: `undefined`): Client key file path -- **NODE\_APP\_INSTANCE**: undefined +- **NODE\_APP\_INSTANCE** (default: `undefined`): Used as default container id, receiver name, sender name, etc. in cluster configurations. ## **License** diff --git a/packages/providers/mqtt/package.json b/packages/providers/mqtt/package.json index f2bef54c..95965c89 100644 --- a/packages/providers/mqtt/package.json +++ b/packages/providers/mqtt/package.json @@ -35,8 +35,8 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "joi": "^17.13.3", - "mqtt": "^5.10.1", - "tslib": "^2.7.0" + "mqtt": "^5.10.3", + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*" diff --git a/packages/providers/mqtt/src/config/default.ts b/packages/providers/mqtt/src/config/default.ts index bc740019..a1e34c60 100644 --- a/packages/providers/mqtt/src/config/default.ts +++ b/packages/providers/mqtt/src/config/default.ts @@ -10,10 +10,17 @@ import { CONFIG_ARTIFACT_ID } from './utils'; // ************************************************************************************************* // #region Default values + +/** + * Used as default container id, receiver name, sender name, etc. in cluster configurations. + * @defaultValue undefined + */ +export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; + const CONFIG_MQTT_URL = 'mqtt://localhost:1883'; const CONFIG_MQTT_PROTOCOL = 'mqtt'; const CONFIG_MQTT_RESUBSCRIBE = true; -const CONFIG_MQTT_CLIENT_ID = process.env['NODE_APP_INSTANCE'] || CONFIG_ARTIFACT_ID; +const CONFIG_MQTT_CLIENT_ID = NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID; const CONFIG_MQTT_KEEPALIVE = 60; export const defaultConfig: Config = { @@ -24,4 +31,3 @@ export const defaultConfig: Config = { keepalive: CONFIG_MQTT_KEEPALIVE, }; // #endregion - diff --git a/packages/providers/mqtt/src/provider/index.ts b/packages/providers/mqtt/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/mqtt/src/provider/index.ts +++ b/packages/providers/mqtt/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/redis/README.md b/packages/providers/redis/README.md index c1cab46a..723cee00 100644 --- a/packages/providers/redis/README.md +++ b/packages/providers/redis/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,7 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** @@ -75,7 +77,7 @@ Checks included in the provider: - **CONFIG\_REDIS\_CONNECTION\_TIMEOUT** (default: `10000`): REDIS connection keepAlive - **CONFIG\_REDIS\_CHECK\_INTERVAL** (default: `60000`): REDIS status check interval - **CONFIG\_REDIS\_DISABLE\_CHECKS** (default: `false`): Disable Redis checks -- **NODE\_APP\_INSTANCE**: undefined +- **NODE\_APP\_INSTANCE** (default: `undefined`): Used as default container id, receiver name, sender name, etc. in cluster configurations. ## **License** diff --git a/packages/providers/redis/package.json b/packages/providers/redis/package.json index 4835d67e..52511809 100644 --- a/packages/providers/redis/package.json +++ b/packages/providers/redis/package.json @@ -38,7 +38,7 @@ "ioredis": "^5.4.1", "joi": "^17.13.3", "redis-errors": "^1.2.0", - "tslib": "^2.7.0" + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*", diff --git a/packages/providers/redis/src/config/env.ts b/packages/providers/redis/src/config/env.ts index 52dd42e2..ef9b8004 100644 --- a/packages/providers/redis/src/config/env.ts +++ b/packages/providers/redis/src/config/env.ts @@ -67,6 +67,12 @@ const CONFIG_REDIS_CHECK_INTERVAL = coerce(process.env['CONFIG_REDIS_CHE */ const CONFIG_REDIS_DISABLE_CHECKS = coerce(process.env['CONFIG_REDIS_DISABLE_CHECKS']); +/** + * Used as default container id, receiver name, sender name, etc. in cluster configurations. + * @defaultValue undefined + */ +export const NODE_APP_INSTANCE = process.env['NODE_APP_INSTANCE']; + export const envBasedConfig: Config = { /** Default Redis instance port */ port: CONFIG_REDIS_PORT, @@ -81,7 +87,7 @@ export const envBasedConfig: Config = { /** TCP KeepAlive in ms. Set to a non-number value to disable keepAlive */ keepAlive: CONFIG_REDIS_KEEPALIVE, /** Connection name*/ - connectionName: process.env['NODE_APP_INSTANCE'] || CONFIG_ARTIFACT_ID, + connectionName: NODE_APP_INSTANCE ?? CONFIG_ARTIFACT_ID, /** Enabled the ready event form Redis instance */ enableReadyCheck: true, /** Enable to send command even when the server is not still ready */ diff --git a/packages/providers/redis/src/provider/Port.ts b/packages/providers/redis/src/provider/Port.ts index 50dee3c3..f5156470 100644 --- a/packages/providers/redis/src/provider/Port.ts +++ b/packages/providers/redis/src/provider/Port.ts @@ -1,334 +1,383 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Layer } from '@mdf.js/core'; -import { Boom, Crash, Multi } from '@mdf.js/crash'; -import { LoggerInstance } from '@mdf.js/logger'; -import IORedis, { RedisOptions } from 'ioredis'; -import { ReplyError } from 'redis-errors'; -import { CONFIG_PROVIDER_BASE_NAME } from '../config'; -import type { MemoryStats, ServerStats, Status } from './Status.i'; -import { Client, Config } from './types'; - -export class Port extends Layer.Provider.Port { - /** Redis connection handler */ - private readonly instance: Client; - /** Event wrapping flags */ - private isWrapped: boolean; - /** Time interval for status check */ - private timeInterval: NodeJS.Timeout | null; - /** Is the first check */ - private isFirstCheck: boolean; - /** Redis healthy state */ - private healthy: boolean; - /** - * Instance is connected, this is necessary to manage the reconnection process. When `start` is - * called, the instance will try to reconnect to the server if the connection is lost, so `start` - * so not be called again. - */ - private connected: boolean; - /** Time interval for ready check request */ - private readonly interval: number; - /** - * Implementation of functionalities of a Redis port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, config.name || CONFIG_PROVIDER_BASE_NAME); - const cleanedOptions = { - ...this.config, - checkInterval: undefined, - disableChecks: undefined, - } as RedisOptions; - this.instance = new IORedis({ ...cleanedOptions, lazyConnect: true }); - // Stryker disable next-line all - this.logger.debug(`New instance of Redis port created: ${this.uuid}`, this.uuid, this.name); - this.interval = this.config.checkInterval as number; - this.isWrapped = false; - this.connected = false; - this.healthy = false; - this.timeInterval = null; - this.isFirstCheck = true; - } - /** Return the underlying port instance */ - public get client(): Client { - return this.instance; - } - /** Return the port state as a boolean value, true if the port is available, false in otherwise */ - public get state(): boolean { - return this.instance.status === 'ready'; - } - /** Start the port, making it available */ - public async start(): Promise { - if (this.connected) { - // Stryker disable next-line all - this.logger.warn(`Port already connected, skipping start`); - return; - } - try { - await this.instance.connect(); - this.connected = true; - this.eventsWrapping(this.instance); - if (!this.timeInterval && !this.config.disableChecks) { - this.timeInterval = setInterval(this.statusCheck, this.interval); - this.statusCheck(); - } - } catch (rawError) { - const cause = Crash.from(rawError, this.uuid); - if (cause.message !== 'Redis is already connecting/connected') { - throw new Crash(`Error performing the connection to Redis instance: ${cause.message}`, { - cause: cause, - }); - } - } - } - /** Stop the port, making it unavailable */ - public async stop(): Promise { - if (!this.connected) { - // Stryker disable next-line all - this.logger.warn(`Port already disconnected, skipping stop`); - return; - } - try { - this.eventsUnwrapping(this.instance); - await this.instance.quit(); - this.connected = false; - } catch (rawError) { - const cause = Crash.from(rawError, this.uuid); - if (cause.message !== 'Connection is closed.') { - throw new Crash(`Error performing the disconnection to Redis instance: ${cause.message}`, { - cause, - }); - } - } finally { - if (this.timeInterval) { - clearInterval(this.timeInterval); - this.timeInterval = null; - } - this.healthy = false; - } - } - /** Close the port, alias to stop */ - public async close(): Promise { - await this.stop(); - } - /** - * Parse string formatted stats from Redis INFO command to JSON - * @param stat - stat to be parsed - * @returns - */ - private parseStats(stat: string): T { - try { - return JSON.parse( - `{${stat - .split('\r\n') - .slice(1, -1) - .map(entry => `"${entry.replace(':', '":"')}"`) - .join(',')}}` - ); - } catch (rawError) { - const error = Crash.from(rawError, this.uuid); - // Stryker disable next-line all - this.logger.warn(`Error parsing instance stats: ${error.message}`, this.uuid, this.name); - return { errorParsing: error.message } as unknown as T; - } - } - /** Get the Memory and Server stats from Redis INFO command */ - private getInfoStats(): Promise { - const stats: Status = { - server: {}, - memory: {}, - } as Status; - return new Promise((resolve, reject) => { - this.instance - .info('server') - .then(result => (stats.server = this.parseStats(result))) - .then(() => this.instance.info('memory')) - .then(result => (stats.memory = this.parseStats(result))) - .then(() => resolve(stats)) - .catch(rawError => { - let error: Crash | Multi | Boom; - if (rawError instanceof ReplyError) { - error = this.errorParse(rawError); - } else { - error = Crash.from(rawError, this.uuid); - } - reject(new Crash(`Error getting the Redis INFO stats`, this.uuid, { cause: error })); - }); - }); - } - /** Check the server and memory status of the redis server instance */ - private readonly statusCheck = (): void => { - this.getInfoStats() - .then(result => { - // Stryker disable next-line all - this.logger.debug(`Status check command performed successfully`, this.uuid, this.name); - this.evaluateStats(result); - }) - .catch(rawError => { - const error = Crash.from(rawError, this.uuid); - this.emit( - 'error', - new Crash(`Error performing the status check of the Redis instance`, this.uuid, { - cause: error, - }) - ); - }) - .finally(() => { - this.isFirstCheck = false; - }); - }; - /** - * Check the results and emit the healthy or unhealthy event - * @param result - The result of the status check - * @returns - */ - private evaluateStats(result: Status): Status { - let message: string | undefined = undefined; - let hasError = false; - let observedValue = '- bytes / - bytes'; - let usage = 0; - const parsingError = result.memory.errorParsing || result.server.errorParsing; - if (parsingError) { - message = `Error parsing the Redis INFO stats: ${parsingError}, please contact with the developers`; - hasError = true; - } else { - usage = - result.memory.maxmemory !== '0' - ? parseFloat(result.memory.used_memory) / parseFloat(result.memory.maxmemory) - : 0; - if (usage >= 1) { - hasError = true; - message = `The system is OOM - used ${result.memory.used_memory_human} - max ${result.memory.maxmemory_human}`; - } else if (usage > 0.9) { - message = `The system is using more than 90% of the available memory`; - } else if (usage > 0.8) { - message = `The system is using more than 80% of the available memory`; - } else { - message = `The system is using ${usage * 100}% of the available memory`; - } - observedValue = `${result.memory.used_memory} / ${result.memory.maxmemory}`; - } - this.addCheck('memory', { - componentId: this.uuid, - observedValue, - observedUnit: 'used memory / max memory', - status: hasError ? 'fail' : usage > 0.8 ? 'warn' : 'pass', - output: message, - time: new Date().toISOString(), - }); - if (hasError && (this.healthy || this.isFirstCheck)) { - this.emit('unhealthy', new Crash(message || 'Unexpected error in the evaluation', this.uuid)); - this.healthy = false; - } else if (!this.healthy) { - this.emit('healthy'); - this.healthy = true; - } - return result; - } - /** - * Auxiliar function to log and emit events - * @param event - event name - * @param args - arguments to be emitted with the event - */ - private onEvent(event: string, ...args: (Crash | Status | number)[]): void { - // Stryker disable next-line all - this.logger.debug(`Event: ${event} was listened`); - for (const arg of args) { - // Stryker disable next-line all - this.logger.silly(`Event ${event} arg: ${arg}`); - } - } - /** Callback function for `connect` event */ - private readonly onConnectEvent = () => this.onEvent('connect'); - /** Callback function for `ready` event */ - private readonly onReadyEvent = () => { - // Stryker disable next-line all - this.logger.debug(`Original event: ready was wrapped to healthy`); - this.emit('healthy'); - }; - /** Callback function for `error` event */ - private readonly onErrorEvent = (rawError: ReplyError) => { - // Stryker disable next-line all - this.logger.error(`Error event: error was wrapped to error`); - const error = this.errorParse(rawError); - this.emit('error', error); - }; - /** Callback function for `close` event */ - private readonly onCloseEvent = () => { - // Stryker disable next-line all - this.logger.debug(`Original event: close was wrapped to unhealthy`); - this.emit('unhealthy', new Crash(`The connection was closed unexpectedly`, this.uuid)); - }; - /** Callback function for `reconnecting` event */ - private readonly onReconnectingEvent = (delay: number) => this.onEvent('reconnecting', delay); - /** Callback function for `end` event */ - private readonly onEndEvent = () => { - // Stryker disable next-line all - this.logger.debug(`Original event: end was wrapped to closed`); - this.emit('closed', new Crash(`The connection was closed intentionally`, this.uuid)); - }; - /** Callback function for `+node` event */ - private readonly onPlusNodeEvent = () => this.onEvent('+node'); - /** Callback function for `-node` event */ - private readonly onMinusNodeEvent = () => this.onEvent('-node'); - /** - * Transforms a ReplyError instance to a Crash instance - * @param error - Error to be parsed - * @returns - */ - private errorParse(error: ReplyError): Crash { - let message = error.message; - /** Redis is requesting AUTH */ - if (error.message.includes('NOAUTH Authentication required')) { - message = 'No authentication config for RDB connection'; - } else if (error.message.includes('ERR invalid password')) { - message = 'Wrong authentication config on RDB connection'; - } - return new Crash(message, this.uuid, { - name: error.name, - cause: Crash.from(error, this.uuid), - info: { - uuid: this.uuid, - redisState: this.instance.status, - }, - }); - } - /** - * Adapts the `ioredis` instance events to standard Port events - * @param instance - Redis instance over which the events should be wrapped - */ - private eventsWrapping(instance: Client): void { - if (this.isWrapped) { - return; - } - instance.on('connect', this.onConnectEvent); - instance.on('ready', this.onReadyEvent); - instance.on('error', this.onErrorEvent); - instance.on('close', this.onCloseEvent); - instance.on('reconnecting', this.onReconnectingEvent); - instance.on('end', this.onEndEvent); - instance.on('+node', this.onPlusNodeEvent); - instance.on('-node', this.onMinusNodeEvent); - this.isWrapped = true; - } - /** - * Clean all the events handlers - * @param instance - Redis instance over which the events should be cleaned - */ - private eventsUnwrapping(instance: Client): void { - instance.off('connect', this.onConnectEvent); - instance.off('ready', this.onReadyEvent); - instance.off('error', this.onErrorEvent); - instance.off('close', this.onCloseEvent); - instance.off('reconnecting', this.onReconnectingEvent); - instance.off('end', this.onEndEvent); - instance.off('+node', this.onPlusNodeEvent); - instance.off('-node', this.onMinusNodeEvent); - this.isWrapped = false; - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Health, Layer } from '@mdf.js/core'; +import { Boom, Crash, Multi } from '@mdf.js/crash'; +import { LoggerInstance } from '@mdf.js/logger'; +import IORedis, { RedisOptions } from 'ioredis'; +import { ReplyError } from 'redis-errors'; +import { CONFIG_PROVIDER_BASE_NAME } from '../config'; +import type { MemoryStats, ServerStats, Status } from './Status.i'; +import { Client, Config } from './types'; + +export class Port extends Layer.Provider.Port { + /** Redis connection handler */ + private readonly instance: Client; + /** Event wrapping flags */ + private isWrapped: boolean; + /** Time interval for status check */ + private timeInterval: NodeJS.Timeout | null; + /** Is the first check */ + private isFirstCheck: boolean; + /** Redis healthy state */ + private healthy: boolean; + /** + * Instance is connected, this is necessary to manage the reconnection process. When `start` is + * called, the instance will try to reconnect to the server if the connection is lost, so `start` + * so not be called again. + */ + private connected: boolean; + /** Time interval for ready check request */ + private readonly interval: number; + /** + * Implementation of functionalities of a Redis port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, config.name ?? CONFIG_PROVIDER_BASE_NAME); + const cleanedOptions = { + ...this.config, + checkInterval: undefined, + disableChecks: undefined, + } as RedisOptions; + this.instance = new IORedis({ ...cleanedOptions, lazyConnect: true }); + // Stryker disable next-line all + this.logger.debug(`New instance of Redis port created: ${this.uuid}`, this.uuid, this.name); + this.interval = this.config.checkInterval as number; + this.isWrapped = false; + this.connected = false; + this.healthy = false; + this.timeInterval = null; + this.isFirstCheck = true; + } + /** Return the underlying port instance */ + public get client(): Client { + return this.instance; + } + /** Return the port state as a boolean value, true if the port is available, false in otherwise */ + public get state(): boolean { + return this.instance.status === 'ready'; + } + /** Start the port, making it available */ + public async start(): Promise { + if (this.connected) { + // Stryker disable next-line all + this.logger.warn(`Port already connected, skipping start`); + return; + } + try { + await this.instance.connect(); + this.connected = true; + this.eventsWrapping(this.instance); + if (!this.timeInterval && !this.config.disableChecks) { + this.timeInterval = setInterval(this.statusCheck, this.interval); + this.statusCheck(); + } + } catch (rawError) { + const cause = Crash.from(rawError, this.uuid); + if (cause.message !== 'Redis is already connecting/connected') { + throw new Crash(`Error performing the connection to Redis instance: ${cause.message}`, { + cause: cause, + }); + } + } + } + /** Stop the port, making it unavailable */ + public async stop(): Promise { + if (!this.connected) { + // Stryker disable next-line all + this.logger.warn(`Port already disconnected, skipping stop`); + return; + } + try { + this.eventsUnwrapping(this.instance); + await this.instance.quit(); + this.connected = false; + } catch (rawError) { + const cause = Crash.from(rawError, this.uuid); + if (cause.message !== 'Connection is closed.') { + throw new Crash(`Error performing the disconnection to Redis instance: ${cause.message}`, { + cause, + }); + } + } finally { + if (this.timeInterval) { + clearInterval(this.timeInterval); + this.timeInterval = null; + } + this.healthy = false; + } + } + /** Close the port, alias to stop */ + public async close(): Promise { + await this.stop(); + } + /** + * Parse string formatted stats from Redis INFO command to JSON + * @param stat - stat to be parsed + * @returns + */ + private parseStats(stat: string): T { + try { + return JSON.parse( + `{${stat + .split('\r\n') + .slice(1, -1) + .map(entry => `"${entry.replace(':', '":"')}"`) + .join(',')}}` + ); + } catch (rawError) { + const error = Crash.from(rawError, this.uuid); + // Stryker disable next-line all + this.logger.warn(`Error parsing instance stats: ${error.message}`, this.uuid, this.name); + return { errorParsing: error.message } as unknown as T; + } + } + /** Get the Memory and Server stats from Redis INFO command */ + private getInfoStats(): Promise { + const stats: Status = { + server: {}, + memory: {}, + } as Status; + return new Promise((resolve, reject) => { + this.instance + .info('server') + .then(result => (stats.server = this.parseStats(result))) + .then(() => this.instance.info('memory')) + .then(result => (stats.memory = this.parseStats(result))) + .then(() => resolve(stats)) + .catch(rawError => { + let error: Crash | Multi | Boom; + if (rawError instanceof ReplyError) { + error = this.errorParse(rawError); + } else { + error = Crash.from(rawError, this.uuid); + } + reject(new Crash(`Error getting the Redis INFO stats`, this.uuid, { cause: error })); + }); + }); + } + /** Check the server and memory status of the redis server instance */ + private readonly statusCheck = (): void => { + this.getInfoStats() + .then(result => { + // Stryker disable next-line all + this.logger.debug(`Status check command performed successfully`, this.uuid, this.name); + this.evaluateStats(result); + }) + .catch(rawError => { + const error = Crash.from(rawError, this.uuid); + this.emit( + 'error', + new Crash(`Error performing the status check of the Redis instance`, this.uuid, { + cause: error, + }) + ); + }) + .finally(() => { + this.isFirstCheck = false; + }); + }; + /** + * Check the results and emit the healthy or unhealthy event + * @param result - The result of the status check + * @returns + */ + private evaluateStats(result: Status): Status { + let message: string | undefined; + let hasError = false; + const observedValue = this.calculateObservedValue(result); + const parsingError = result.memory.errorParsing ?? result.server.errorParsing; + let usage = 0; + + if (parsingError) { + message = `Error parsing the Redis INFO stats: ${parsingError}, please contact with the developers`; + hasError = true; + } else { + usage = this.calculateMemoryUsage(result); + ({ hasError, message } = this.determineMemoryStatus(usage, result, hasError)); + } + + const usageStatus: Health.Status = usage > 0.8 ? 'warn' : 'pass'; + const status = hasError ? 'fail' : usageStatus; + + this.addCheck('memory', { + componentId: this.uuid, + observedValue, + observedUnit: 'used memory / max memory', + status, + output: message, + time: new Date().toISOString(), + }); + + this.emitHealthStatus(hasError, message); + return result; + } + /** + * Calculate the observed value for memory usage. + * @param result - The result of the status check + * @returns The observed value as a string. + */ + private calculateObservedValue(result: Status): string { + if (!result.memory.used_memory && !result.memory.maxmemory) { + return '- bytes / - bytes'; + } + return `${result.memory.used_memory} / ${result.memory.maxmemory}`; + } + /** + * Calculate memory usage percentage. + * @param result - The result of the status check + * @returns The memory usage as a percentage. + */ + private calculateMemoryUsage(result: Status): number { + return result.memory.maxmemory !== '0' + ? parseFloat(result.memory.used_memory) / parseFloat(result.memory.maxmemory) + : 0; + } + /** + * Determine the memory status and create an appropriate message. + * @param usage - The memory usage percentage. + * @param result - The result of the status check. + * @param hasError - A flag indicating if there's an error. + * @returns An object containing the updated hasError flag and message. + */ + private determineMemoryStatus( + usage: number, + result: Status, + hasError: boolean + ): { hasError: boolean; message: string | undefined } { + let message: string | undefined; + + if (usage >= 1) { + hasError = true; + message = `The system is OOM - used ${result.memory.used_memory_human} - max ${result.memory.maxmemory_human}`; + } else if (usage > 0.9) { + message = `The system is using more than 90% of the available memory`; + } else if (usage > 0.8) { + message = `The system is using more than 80% of the available memory`; + } else { + message = `The system is using ${(usage * 100).toFixed(2)}% of the available memory`; + } + + return { hasError, message }; + } + /** + * Emit the appropriate health status event based on the current state. + * @param hasError - Indicates if an error was detected. + * @param message - The error message, if any. + */ + private emitHealthStatus(hasError: boolean, message: string | undefined): void { + if (hasError && (this.healthy || this.isFirstCheck)) { + this.emit('unhealthy', new Crash(message ?? 'Unexpected error in the evaluation', this.uuid)); + this.healthy = false; + } else if (!hasError && !this.healthy) { + this.emit('healthy'); + this.healthy = true; + } + } + /** + * Auxiliar function to log and emit events + * @param event - event name + * @param args - arguments to be emitted with the event + */ + private onEvent(event: string, ...args: (Crash | Status | number)[]): void { + // Stryker disable next-line all + this.logger.debug(`Event: ${event} was listened`); + for (const arg of args) { + // Stryker disable next-line all + this.logger.silly(`Event ${event} arg: ${JSON.stringify(arg)}`); + } + } + /** Callback function for `connect` event */ + private readonly onConnectEvent = () => this.onEvent('connect'); + /** Callback function for `ready` event */ + private readonly onReadyEvent = () => { + // Stryker disable next-line all + this.logger.debug(`Original event: ready was wrapped to healthy`); + this.emit('healthy'); + }; + /** Callback function for `error` event */ + private readonly onErrorEvent = (rawError: ReplyError) => { + // Stryker disable next-line all + this.logger.error(`Error event: error was wrapped to error`); + const error = this.errorParse(rawError); + this.emit('error', error); + }; + /** Callback function for `close` event */ + private readonly onCloseEvent = () => { + // Stryker disable next-line all + this.logger.debug(`Original event: close was wrapped to unhealthy`); + this.emit('unhealthy', new Crash(`The connection was closed unexpectedly`, this.uuid)); + }; + /** Callback function for `reconnecting` event */ + private readonly onReconnectingEvent = (delay: number) => this.onEvent('reconnecting', delay); + /** Callback function for `end` event */ + private readonly onEndEvent = () => { + // Stryker disable next-line all + this.logger.debug(`Original event: end was wrapped to closed`); + this.emit('closed', new Crash(`The connection was closed intentionally`, this.uuid)); + }; + /** Callback function for `+node` event */ + private readonly onPlusNodeEvent = () => this.onEvent('+node'); + /** Callback function for `-node` event */ + private readonly onMinusNodeEvent = () => this.onEvent('-node'); + /** + * Transforms a ReplyError instance to a Crash instance + * @param error - Error to be parsed + * @returns + */ + private errorParse(error: ReplyError): Crash { + let message = error.message; + /** Redis is requesting AUTH */ + if (error.message.includes('NOAUTH Authentication required')) { + message = 'No authentication config for RDB connection'; + } else if (error.message.includes('ERR invalid password')) { + message = 'Wrong authentication config on RDB connection'; + } + return new Crash(message, this.uuid, { + name: error.name, + cause: Crash.from(error, this.uuid), + info: { + uuid: this.uuid, + redisState: this.instance.status, + }, + }); + } + /** + * Adapts the `ioredis` instance events to standard Port events + * @param instance - Redis instance over which the events should be wrapped + */ + private eventsWrapping(instance: Client): void { + if (this.isWrapped) { + return; + } + instance.on('connect', this.onConnectEvent); + instance.on('ready', this.onReadyEvent); + instance.on('error', this.onErrorEvent); + instance.on('close', this.onCloseEvent); + instance.on('reconnecting', this.onReconnectingEvent); + instance.on('end', this.onEndEvent); + instance.on('+node', this.onPlusNodeEvent); + instance.on('-node', this.onMinusNodeEvent); + this.isWrapped = true; + } + /** + * Clean all the events handlers + * @param instance - Redis instance over which the events should be cleaned + */ + private eventsUnwrapping(instance: Client): void { + instance.off('connect', this.onConnectEvent); + instance.off('ready', this.onReadyEvent); + instance.off('error', this.onErrorEvent); + instance.off('close', this.onCloseEvent); + instance.off('reconnecting', this.onReconnectingEvent); + instance.off('end', this.onEndEvent); + instance.off('+node', this.onPlusNodeEvent); + instance.off('-node', this.onMinusNodeEvent); + this.isWrapped = false; + } +} diff --git a/packages/providers/redis/src/provider/Provider.test.ts b/packages/providers/redis/src/provider/Provider.test.ts index 6cdfcfa1..64b07ed4 100644 --- a/packages/providers/redis/src/provider/Provider.test.ts +++ b/packages/providers/redis/src/provider/Provider.test.ts @@ -1,497 +1,499 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Layer } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { LoggerInstance } from '@mdf.js/logger'; -import IORedis from 'ioredis'; -import { ReplyError } from 'redis-errors'; -import { Factory } from './Factory'; -import { Port } from './Port'; -import { Config } from './types'; - -const DEFAULT_CONFIG: Config = { - port: 28910, - host: '127.0.0.1', - db: 0, - family: 4, - keepAlive: 10000, - connectionName: 'myRedis', - enableReadyCheck: true, - enableOfflineQueue: true, - connectTimeout: 10000, - autoResubscribe: true, - autoResendUnfulfilledCommands: true, - lazyConnect: true, - keyPrefix: '', - readOnly: false, - retryStrategy: (times: number): number => { - return Math.min(times * 2000, 60000); - }, - reconnectOnError: (error: ReplyError): boolean => { - if (error.message.includes('ERR invalid password')) { - return false; - } - return true; - }, - showFriendlyErrorStack: true, - checkInterval: 10000, -}; -class FakeLogger { - public entry?: string; - public debug(value: string): void { - this.entry = value; - } - public info(value: string): void { - this.entry = value; - } - public error(value: string): void { - this.entry = value; - } - public crash(error: Crash): void { - this.entry = error.message; - } - public warn(value: string): void { - this.entry = value; - } - public silly(value: string): void { - this.entry = value; - } -} - -const memory = - '# Memory' + - '\r\nused_memory:1090104' + - '\r\nused_memory_human:1.04M' + - '\r\nmaxmemory:0' + - '\r\nmaxmemory_human:0B' + - '\r\n'; -const memoryProblem = - '# Memory' + - '\r\nused_memory:1090104' + - '\r\nused_memory_human:1.04M' + - '\r\nmaxmemory:1' + - '\r\nmaxmemory_human:1B' + - '\r\n'; -const memoryUnparseable = - '# Memory' + - '\r\nused_memory:1090104' + - '\r\nused_memory_human:1.04M' + - '\r\nmaxmemory1' + - '\r\nmaxmemory_human:1B' + - '\r\n'; -describe('#Port #Redis', () => { - describe('#Happy path', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - it('Should create provider using the factory instance with default configuration', () => { - const provider = Factory.create(); - expect(provider).toBeDefined(); - expect(provider).toBeInstanceOf(Layer.Provider.Manager); - expect(provider.client).toBeInstanceOf(IORedis); - expect(provider.state).toEqual('stopped'); - //@ts-ignore - Test environment - expect(provider.options.useEnvironment).toBeTruthy(); - const checks = provider.checks; - expect(checks).toEqual({ - 'redis:status': [ - { - componentId: checks['redis:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['redis:status'][0].time, - }, - ], - }); - }, 300); - it('Should create provider using the factory instance with a configuration', () => { - const provider = Factory.create({ - useEnvironment: false, - name: 'redis', - logger: new FakeLogger() as LoggerInstance, - config: {}, - }); - expect(provider).toBeDefined(); - expect(provider).toBeInstanceOf(Layer.Provider.Manager); - expect(provider.client).toBeInstanceOf(IORedis); - expect(provider.state).toEqual('stopped'); - //@ts-ignore - Test environment - expect(provider.options.useEnvironment).toBeFalsy(); - const checks = provider.checks; - expect(checks).toEqual({ - 'redis:status': [ - { - componentId: checks['redis:status'][0].componentId, - componentType: 'database', - observedValue: 'stopped', - output: undefined, - status: 'warn', - time: checks['redis:status'][0].time, - }, - ], - }); - }, 300); - it('Should create the instance with default configuration', () => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - expect(port.client).toBeInstanceOf(IORedis); - expect(port.state).toBeFalsy(); - expect(port.checks).toEqual({}); - }, 300); - it('Should start the status check interval if connection is established, and stop it on disconnect', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - port.on('error', error => { - throw error; - }); - port.on('healthy', () => { - const checks = port.checks; - expect(checks).toEqual({ - memory: [ - { - componentId: checks['memory'][0].componentId, - observedUnit: 'used memory / max memory', - observedValue: '1090104 / 0', - output: `The system is using 0% of the available memory`, - status: 'pass', - time: checks['memory'][0].time, - }, - ], - }); - // @ts-ignore - Test environment - jest.replaceProperty(port.instance, 'status', 'ready'); - // This is to test that can not wrap method twice - port.start().then(); - expect(port.state).toBeTruthy(); - // @ts-ignore - Test environment - expect(port.timeInterval).toBeDefined(); - expect(port.client.listenerCount('connect')).toEqual(1); - port.close().then(); - expect(port.client.listenerCount('connect')).toEqual(0); - done(); - }); - port.start().then(() => { - expect(port.client.listenerCount('connect')).toEqual(1); - }); - }, 300); - it('Should perform the event wrapping properly for "error" on ReplyError', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - port.on('error', error => { - expect(error.message).toEqual('myError'); - done(); - }); - port.start().then(() => { - port.client.emit('error', new ReplyError('myError')); - }); - }, 300); - it('Should perform the event wrapping properly for "error" on Crash', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - port.on('error', error => { - expect(error.message).toEqual('myError'); - done(); - }); - port.start().then(() => { - port.client.emit('error', new Crash('myError')); - }); - }, 300); - it('Should perform the event wrapping properly for "end"', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - port.on('closed', error => { - expect(error?.message).toEqual('The connection was closed intentionally'); - done(); - }); - port.start().then(() => { - port.client.emit('end'); - }); - }, 300); - it('Should resolve if try to connect when the instance is already connected', async () => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - const mock = jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'info').mockResolvedValue('OK'); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - await port.start(); - await port.start(); - await port.close(); - expect(mock).toHaveBeenCalledTimes(1); - }, 300); - it('Should resolve if try to connect and an error is throw by the connect with the message "Redis is already connecting/connected"', async () => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - const mock = jest - .spyOn(port.client, 'connect') - .mockRejectedValue(new Error('Redis is already connecting/connected')); - jest.spyOn(port.client, 'info').mockResolvedValue('OK'); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - await port.start(); - await port.close(); - expect(mock).toHaveBeenCalledTimes(1); - }, 300); - it('Should resolve if try to disconnect when the instance is already disconnected', async () => { - try { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - await port.close(); - } catch (error) { - console.log(error); - throw new Error('Should not be here'); - } - }, 300); - it('Should resolve if try to disconnect and an error is throw by the quit method with the message "Connection is closed."', async () => { - try { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - // @ts-ignore - Test environment - jest.replaceProperty(port, 'connected', true); - const mock = jest - .spyOn(port.client, 'quit') - .mockRejectedValue(new Error('Connection is closed.')); - jest.spyOn(port.client, 'info').mockResolvedValue(memory); - await port.close(); - expect(mock).toHaveBeenCalledTimes(1); - } catch (error) { - console.log(error); - throw new Error('Should not be here'); - } - }, 300); - }); - describe('#Sad path', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - it('Should rejects if try to connect and the methods rejects', async () => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockRejectedValue(new Error('myError')); - try { - //@ts-ignore - Test environment - expect(port.timeInterval).toBeNull(); - await port.start(); - throw new Error('Should not be here'); - } catch (rawError: any) { - //@ts-ignore - Test environment - expect(port.timeInterval).toBeDefined(); - expect(rawError).toBeInstanceOf(Crash); - expect(rawError.message).toEqual( - 'Error performing the connection to Redis instance: myError' - ); - expect(rawError.cause).toBeInstanceOf(Error); - expect(rawError.cause?.message).toEqual('myError'); - } - }, 300); - it('Should rejects if try to disconnect and the methods rejects', async () => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - // @ts-ignore - Test environment - jest.replaceProperty(port, 'connected', true); - jest.spyOn(port.client, 'quit').mockRejectedValue(new Error('myError')); - try { - await port.stop(); - throw new Error('Should not be here'); - } catch (rawError: any) { - expect(rawError).toBeInstanceOf(Crash); - expect(rawError.message).toEqual( - 'Error performing the disconnection to Redis instance: myError' - ); - expect(rawError.cause).toBeInstanceOf(Error); - expect(rawError.cause?.message).toEqual('myError'); - } - }, 300); - it('Should emit healthy and unhealthy events properly', done => { - const port = new Port( - { ...DEFAULT_CONFIG, lazyConnect: false, checkInterval: 50 }, - new FakeLogger() as LoggerInstance - ); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - let count = 0; - jest.spyOn(port.client, 'info').mockImplementation(() => { - count++; - if (count === 1 || count === 2 || count === 3 || count > 4) { - return Promise.resolve(memory); - } else if (count === 4) { - return Promise.resolve(memoryProblem); - } else { - return Promise.resolve(memory); - } - }); - port.on('error', error => { - throw error; - }); - let wasUnhealthy = false; - let wasHealthy = false; - port.on('healthy', () => { - wasHealthy = true; - if (wasHealthy && wasUnhealthy) { - port.close().then(done); - } - }); - port.on('unhealthy', error => { - wasUnhealthy = true; - }); - port.start().then(); - }, 300); - it('Should emit unhealthy event if there is a problem in the memory resources', done => { - const port = new Port( - { ...DEFAULT_CONFIG, checkInterval: 50 }, - new FakeLogger() as LoggerInstance - ); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memoryProblem); - port.on('error', error => { - throw error; - }); - port.on('unhealthy', error => { - expect(error.message).toEqual('The system is OOM - used 1.04M - max 1B'); - const checks = port.checks; - expect(checks).toEqual({ - memory: [ - { - componentId: checks['memory'][0].componentId, - observedUnit: 'used memory / max memory', - observedValue: '1090104 / 1', - output: 'The system is OOM - used 1.04M - max 1B', - status: 'fail', - time: checks['memory'][0].time, - }, - ], - }); - port.close().then(); - done(); - }); - port.start().then(() => { - expect(port.client.listenerCount('connect')).toEqual(1); - }); - }, 300); - it('Should emit unhealthy event if there is a problem parsing the results', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockResolvedValue(memoryUnparseable); - port.on('error', error => { - throw error; - }); - port.on('unhealthy', error => { - expect(error.message).toContain('Error parsing the Redis INFO stats: Unexpected token'); - const checks = port.checks; - expect(checks).toEqual({ - memory: [ - { - componentId: checks['memory'][0].componentId, - observedUnit: 'used memory / max memory', - observedValue: '- bytes / - bytes', - output: error.message, - status: 'fail', - time: checks['memory'][0].time, - }, - ], - }); - port.close().then(); - done(); - }); - port.start().then(() => { - expect(port.client.listenerCount('connect')).toEqual(1); - }); - }, 300); - it('Should emit unhealthy event if there is a problem in the memory resources the second time that is checked', done => { - const port = new Port( - { ...DEFAULT_CONFIG, checkInterval: 50 }, - new FakeLogger() as LoggerInstance - ); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest - .spyOn(port.client, 'info') - .mockResolvedValueOnce(memory) - .mockResolvedValueOnce(memory) - .mockResolvedValueOnce(memoryProblem) - .mockResolvedValueOnce(memoryProblem); - port.on('error', error => { - throw error; - }); - port.on('unhealthy', error => { - expect(error.message).toEqual('The system is OOM - used 1.04M - max 1B'); - port.close().then(); - done(); - }); - port.start().then(() => { - expect(port.client.listenerCount('connect')).toEqual(1); - }); - }, 300); - it('Should emit error event if there is a problem getting the info from the server', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockRejectedValue(new Error('myError')); - port.on('error', error => { - expect(error.message).toEqual('Error performing the status check of the Redis instance'); - port.close().then(); - done(); - }); - port.start().then(); - }, 300); - it('Should emit error event if there is a problem getting the info from the server as ReplyError "NOAUTH Authentication required"', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest - .spyOn(port.client, 'info') - .mockRejectedValue(new ReplyError('NOAUTH Authentication required')); - port.on('error', (error: any) => { - expect(error.message).toEqual('Error performing the status check of the Redis instance'); - expect(error.cause.message).toEqual('Error getting the Redis INFO stats'); - expect(error.cause.cause.message).toEqual('No authentication config for RDB connection'); - port.close().then(); - done(); - }); - port.start().then(); - }, 300); - it('Should emit error event if there is a problem getting the info from the server as ReplyError "ERR invalid password"', done => { - const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); - expect(port).toBeDefined(); - jest.spyOn(port.client, 'connect').mockResolvedValue(); - jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); - jest.spyOn(port.client, 'info').mockRejectedValue(new ReplyError('ERR invalid password')); - port.on('error', (error: any) => { - expect(error.message).toEqual('Error performing the status check of the Redis instance'); - expect(error.cause.message).toEqual('Error getting the Redis INFO stats'); - expect(error.cause.cause.message).toEqual('Wrong authentication config on RDB connection'); - port.close().then(); - done(); - }); - port.start().then(); - }, 300); - }); -}); +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { LoggerInstance } from '@mdf.js/logger'; +import IORedis from 'ioredis'; +import { ReplyError } from 'redis-errors'; +import { Factory } from './Factory'; +import { Port } from './Port'; +import { Config } from './types'; + +const DEFAULT_CONFIG: Config = { + port: 28910, + host: '127.0.0.1', + db: 0, + family: 4, + keepAlive: 10000, + connectionName: 'myRedis', + enableReadyCheck: true, + enableOfflineQueue: true, + connectTimeout: 10000, + autoResubscribe: true, + autoResendUnfulfilledCommands: true, + lazyConnect: true, + keyPrefix: '', + readOnly: false, + retryStrategy: (times: number): number => { + return Math.min(times * 2000, 60000); + }, + reconnectOnError: (error: ReplyError): boolean => { + if (error.message.includes('ERR invalid password')) { + return false; + } + return true; + }, + showFriendlyErrorStack: true, + checkInterval: 10000, +}; +class FakeLogger { + public entry?: string; + public debug(value: string): void { + this.entry = value; + } + public info(value: string): void { + this.entry = value; + } + public error(value: string): void { + this.entry = value; + } + public crash(error: Crash): void { + this.entry = error.message; + } + public warn(value: string): void { + this.entry = value; + } + public silly(value: string): void { + this.entry = value; + } +} + +const memory = + '# Memory' + + '\r\nused_memory:1090104' + + '\r\nused_memory_human:1.04M' + + '\r\nmaxmemory:0' + + '\r\nmaxmemory_human:0B' + + '\r\n'; +const memoryProblem = + '# Memory' + + '\r\nused_memory:1090104' + + '\r\nused_memory_human:1.04M' + + '\r\nmaxmemory:1' + + '\r\nmaxmemory_human:1B' + + '\r\n'; +const memoryUnparsable = + '# Memory' + + '\r\nused_memory:1090104' + + '\r\nused_memory_human:1.04M' + + '\r\nmaxmemory1' + + '\r\nmaxmemory_human:1B' + + '\r\n'; +describe('#Port #Redis', () => { + describe('#Happy path', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('Should create provider using the factory instance with default configuration', () => { + const provider = Factory.create(); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(Layer.Provider.Manager); + expect(provider.client).toBeInstanceOf(IORedis); + expect(provider.state).toEqual('stopped'); + //@ts-ignore - Test environment + expect(provider.options.useEnvironment).toBeTruthy(); + const checks = provider.checks; + expect(checks).toEqual({ + 'redis:status': [ + { + componentId: checks['redis:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['redis:status'][0].time, + }, + ], + }); + }, 300); + it('Should create provider using the factory instance with a configuration', () => { + const provider = Factory.create({ + useEnvironment: false, + name: 'redis', + logger: new FakeLogger() as LoggerInstance, + config: {}, + }); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(Layer.Provider.Manager); + expect(provider.client).toBeInstanceOf(IORedis); + expect(provider.state).toEqual('stopped'); + //@ts-ignore - Test environment + expect(provider.options.useEnvironment).toBeFalsy(); + const checks = provider.checks; + expect(checks).toEqual({ + 'redis:status': [ + { + componentId: checks['redis:status'][0].componentId, + componentType: 'database', + observedValue: 'stopped', + output: undefined, + status: 'warn', + time: checks['redis:status'][0].time, + }, + ], + }); + }, 300); + it('Should create the instance with default configuration', () => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + expect(port.client).toBeInstanceOf(IORedis); + expect(port.state).toBeFalsy(); + expect(port.checks).toEqual({}); + }, 300); + it('Should start the status check interval if connection is established, and stop it on disconnect', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + port.on('error', error => { + throw error; + }); + port.on('healthy', () => { + const checks = port.checks; + expect(checks).toEqual({ + memory: [ + { + componentId: checks['memory'][0].componentId, + observedUnit: 'used memory / max memory', + observedValue: '1090104 / 0', + output: `The system is using 0.00% of the available memory`, + status: 'pass', + time: checks['memory'][0].time, + }, + ], + }); + // @ts-ignore - Test environment + jest.replaceProperty(port.instance, 'status', 'ready'); + // This is to test that can not wrap method twice + port.start().then(); + expect(port.state).toBeTruthy(); + // @ts-ignore - Test environment + expect(port.timeInterval).toBeDefined(); + expect(port.client.listenerCount('connect')).toEqual(1); + port.close().then(); + expect(port.client.listenerCount('connect')).toEqual(0); + done(); + }); + port.start().then(() => { + expect(port.client.listenerCount('connect')).toEqual(1); + }); + }, 300); + it('Should perform the event wrapping properly for "error" on ReplyError', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + port.on('error', error => { + expect(error.message).toEqual('myError'); + done(); + }); + port.start().then(() => { + port.client.emit('error', new ReplyError('myError')); + }); + }, 300); + it('Should perform the event wrapping properly for "error" on Crash', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + port.on('error', error => { + expect(error.message).toEqual('myError'); + done(); + }); + port.start().then(() => { + port.client.emit('error', new Crash('myError')); + }); + }, 300); + it('Should perform the event wrapping properly for "end"', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + port.on('closed', error => { + expect(error?.message).toEqual('The connection was closed intentionally'); + done(); + }); + port.start().then(() => { + port.client.emit('end'); + }); + }, 300); + it('Should resolve if try to connect when the instance is already connected', async () => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + const mock = jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'info').mockResolvedValue('OK'); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + await port.start(); + await port.start(); + await port.close(); + expect(mock).toHaveBeenCalledTimes(1); + }, 300); + it('Should resolve if try to connect and an error is throw by the connect with the message "Redis is already connecting/connected"', async () => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + const mock = jest + .spyOn(port.client, 'connect') + .mockRejectedValue(new Error('Redis is already connecting/connected')); + jest.spyOn(port.client, 'info').mockResolvedValue('OK'); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + await port.start(); + await port.close(); + expect(mock).toHaveBeenCalledTimes(1); + }, 300); + it('Should resolve if try to disconnect when the instance is already disconnected', async () => { + try { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + await port.close(); + } catch (error) { + console.log(error); + throw new Error('Should not be here'); + } + }, 300); + it('Should resolve if try to disconnect and an error is throw by the quit method with the message "Connection is closed."', async () => { + try { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + // @ts-ignore - Test environment + jest.replaceProperty(port, 'connected', true); + const mock = jest + .spyOn(port.client, 'quit') + .mockRejectedValue(new Error('Connection is closed.')); + jest.spyOn(port.client, 'info').mockResolvedValue(memory); + await port.close(); + expect(mock).toHaveBeenCalledTimes(1); + } catch (error) { + console.log(error); + throw new Error('Should not be here'); + } + }, 300); + }); + describe('#Sad path', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('Should rejects if try to connect and the methods rejects', async () => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockRejectedValue(new Error('myError')); + try { + //@ts-ignore - Test environment + expect(port.timeInterval).toBeNull(); + await port.start(); + throw new Error('Should not be here'); + } catch (rawError: any) { + //@ts-ignore - Test environment + expect(port.timeInterval).toBeDefined(); + expect(rawError).toBeInstanceOf(Crash); + expect(rawError.message).toEqual( + 'Error performing the connection to Redis instance: myError' + ); + expect(rawError.cause).toBeInstanceOf(Error); + expect(rawError.cause?.message).toEqual('myError'); + } + }, 300); + it('Should rejects if try to disconnect and the methods rejects', async () => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + // @ts-ignore - Test environment + jest.replaceProperty(port, 'connected', true); + jest.spyOn(port.client, 'quit').mockRejectedValue(new Error('myError')); + try { + await port.stop(); + throw new Error('Should not be here'); + } catch (rawError: any) { + expect(rawError).toBeInstanceOf(Crash); + expect(rawError.message).toEqual( + 'Error performing the disconnection to Redis instance: myError' + ); + expect(rawError.cause).toBeInstanceOf(Error); + expect(rawError.cause?.message).toEqual('myError'); + } + }, 300); + it('Should emit healthy and unhealthy events properly', done => { + const port = new Port( + { ...DEFAULT_CONFIG, lazyConnect: false, checkInterval: 50 }, + new FakeLogger() as LoggerInstance + ); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + let count = 0; + jest.spyOn(port.client, 'info').mockImplementation(() => { + count++; + if (count === 1 || count === 2 || count === 3 || count > 4) { + return Promise.resolve(memory); + } else if (count === 4) { + return Promise.resolve(memoryProblem); + } else { + return Promise.resolve(memory); + } + }); + port.on('error', error => { + throw error; + }); + let wasUnhealthy = false; + let wasHealthy = false; + port.on('healthy', () => { + wasHealthy = true; + if (wasHealthy && wasUnhealthy) { + port.close().then(done); + } + }); + port.on('unhealthy', error => { + wasUnhealthy = true; + }); + port.start().then(); + }, 300); + it('Should emit unhealthy event if there is a problem in the memory resources', done => { + const port = new Port( + { ...DEFAULT_CONFIG, checkInterval: 50 }, + new FakeLogger() as LoggerInstance + ); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memoryProblem); + port.on('error', error => { + throw error; + }); + port.on('unhealthy', error => { + expect(error.message).toEqual('The system is OOM - used 1.04M - max 1B'); + const checks = port.checks; + expect(checks).toEqual({ + memory: [ + { + componentId: checks['memory'][0].componentId, + observedUnit: 'used memory / max memory', + observedValue: '1090104 / 1', + output: 'The system is OOM - used 1.04M - max 1B', + status: 'fail', + time: checks['memory'][0].time, + }, + ], + }); + port.close().then(); + done(); + }); + port.start().then(() => { + expect(port.client.listenerCount('connect')).toEqual(1); + }); + }, 300); + it('Should emit unhealthy event if there is a problem parsing the results', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockResolvedValue(memoryUnparsable); + port.on('error', error => { + throw error; + }); + port.on('unhealthy', error => { + expect(error.message).toContain( + "Error parsing the Redis INFO stats: Expected ':' after property name in JSON at position 65 (line 1 column 66), please contact with the developers" + ); + const checks = port.checks; + expect(checks).toEqual({ + memory: [ + { + componentId: checks['memory'][0].componentId, + observedUnit: 'used memory / max memory', + observedValue: '- bytes / - bytes', + output: error.message, + status: 'fail', + time: checks['memory'][0].time, + }, + ], + }); + port.close().then(); + done(); + }); + port.start().then(() => { + expect(port.client.listenerCount('connect')).toEqual(1); + }); + }, 300); + it('Should emit unhealthy event if there is a problem in the memory resources the second time that is checked', done => { + const port = new Port( + { ...DEFAULT_CONFIG, checkInterval: 50 }, + new FakeLogger() as LoggerInstance + ); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest + .spyOn(port.client, 'info') + .mockResolvedValueOnce(memory) + .mockResolvedValueOnce(memory) + .mockResolvedValueOnce(memoryProblem) + .mockResolvedValueOnce(memoryProblem); + port.on('error', error => { + throw error; + }); + port.on('unhealthy', error => { + expect(error.message).toEqual('The system is OOM - used 1.04M - max 1B'); + port.close().then(); + done(); + }); + port.start().then(() => { + expect(port.client.listenerCount('connect')).toEqual(1); + }); + }, 300); + it('Should emit error event if there is a problem getting the info from the server', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockRejectedValue(new Error('myError')); + port.on('error', error => { + expect(error.message).toEqual('Error performing the status check of the Redis instance'); + port.close().then(); + done(); + }); + port.start().then(); + }, 300); + it('Should emit error event if there is a problem getting the info from the server as ReplyError "NOAUTH Authentication required"', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest + .spyOn(port.client, 'info') + .mockRejectedValue(new ReplyError('NOAUTH Authentication required')); + port.on('error', (error: any) => { + expect(error.message).toEqual('Error performing the status check of the Redis instance'); + expect(error.cause.message).toEqual('Error getting the Redis INFO stats'); + expect(error.cause.cause.message).toEqual('No authentication config for RDB connection'); + port.close().then(); + done(); + }); + port.start().then(); + }, 300); + it('Should emit error event if there is a problem getting the info from the server as ReplyError "ERR invalid password"', done => { + const port = new Port(DEFAULT_CONFIG, new FakeLogger() as LoggerInstance); + expect(port).toBeDefined(); + jest.spyOn(port.client, 'connect').mockResolvedValue(); + jest.spyOn(port.client, 'quit').mockResolvedValue('OK'); + jest.spyOn(port.client, 'info').mockRejectedValue(new ReplyError('ERR invalid password')); + port.on('error', (error: any) => { + expect(error.message).toEqual('Error performing the status check of the Redis instance'); + expect(error.cause.message).toEqual('Error getting the Redis INFO stats'); + expect(error.cause.cause.message).toEqual('Wrong authentication config on RDB connection'); + port.close().then(); + done(); + }); + port.start().then(); + }, 300); + }); +}); diff --git a/packages/providers/redis/src/provider/index.ts b/packages/providers/redis/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/redis/src/provider/index.ts +++ b/packages/providers/redis/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/s3/README.md b/packages/providers/s3/README.md index 3a0e3109..cfca252b 100644 --- a/packages/providers/s3/README.md +++ b/packages/providers/s3/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/providers/s3/package.json b/packages/providers/s3/package.json index 7426c5f8..9ba9c7ee 100644 --- a/packages/providers/s3/package.json +++ b/packages/providers/s3/package.json @@ -1,50 +1,52 @@ -{ - "name": "@mdf.js/s3-provider", - "version": "0.0.1", - "description": "MMS - S3 Port for Javascript/Typescript", - "keywords": [ - "NodeJS", - "provider", - "MMS", - "s3" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/providers/s3" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/s3/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.667.0", - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "joi": "^17.13.1", - "tslib": "^2.7.0" - }, - "devDependencies": { - "@mdf.js/repo-config": "*" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/s3-provider", + "version": "0.0.1", + "description": "MMS - S3 Port for Javascript/Typescript", + "keywords": [ + "NodeJS", + "provider", + "MMS", + "s3" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/providers/s3" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/s3/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.712.0", + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/logger": "*", + "@smithy/node-http-handler": "^3.3.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "joi": "^17.13.1", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@mdf.js/repo-config": "*" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/providers/s3/src/provider/Port.ts b/packages/providers/s3/src/provider/Port.ts index 786e8dbb..3a50e725 100644 --- a/packages/providers/s3/src/provider/Port.ts +++ b/packages/providers/s3/src/provider/Port.ts @@ -1,58 +1,60 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Layer } from '@mdf.js/core'; -import { LoggerInstance } from '@mdf.js/logger'; -import { CONFIG_PROVIDER_BASE_NAME } from '../config'; -import { Client, Config } from './types'; - -export class Port extends Layer.Provider.Port { - /** S3 connection handler */ - private readonly instance: Client; - /** Connection flag */ - private connected: boolean; - - /** - * Implementation of functionalities of a S3 port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, config.serviceId || CONFIG_PROVIDER_BASE_NAME); - this.logger.info(`config: ${JSON.stringify(config)}`); - this.instance = new Client(config); - // Stryker disable next-line all - this.logger.debug(`New instance of S3 port created: ${this.uuid}`, this.uuid, this.name); - this.connected = false; - } - /** Return the underlying port instance */ - public get client(): Client { - return this.instance; - } - /** Return the port state as a boolean value, true if the port is available, false in otherwise */ - public get state(): boolean { - return this.connected; - } - /** Start the port, making it available */ - public async start(): Promise { - if (!this.connected) { - this.connected = true; - } - return Promise.resolve(); - } - /** Stop the port, making it unavailable */ - public async stop(): Promise { - if (this.connected) { - this.instance.destroy(); - this.connected = false; - } - return Promise.resolve(); - } - /** Close the port, alias to stop */ - public async close(): Promise { - await this.stop(); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Layer } from '@mdf.js/core'; +import { LoggerInstance } from '@mdf.js/logger'; +import { CONFIG_PROVIDER_BASE_NAME } from '../config'; +import { Client, Config } from './types'; + +export class Port extends Layer.Provider.Port { + /** S3 connection handler */ + private readonly instance: Client; + /** Connection flag */ + private connected: boolean; + + /** + * Implementation of functionalities of a S3 port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, config.serviceId || CONFIG_PROVIDER_BASE_NAME); + this.logger.silly( + `config: ${JSON.stringify({ ...config, credentials: { accessKeyId: '***', secretAccess: '***' } })}` + ); + this.instance = new Client(config); + // Stryker disable next-line all + this.logger.debug(`New instance of S3 port created: ${this.uuid}`, this.uuid, this.name); + this.connected = false; + } + /** Return the underlying port instance */ + public get client(): Client { + return this.instance; + } + /** Return the port state as a boolean value, true if the port is available, false in otherwise */ + public get state(): boolean { + return this.connected; + } + /** Start the port, making it available */ + public async start(): Promise { + if (!this.connected) { + this.connected = true; + } + return Promise.resolve(); + } + /** Stop the port, making it unavailable */ + public async stop(): Promise { + if (this.connected) { + this.instance.destroy(); + this.connected = false; + } + return Promise.resolve(); + } + /** Close the port, alias to stop */ + public async close(): Promise { + await this.stop(); + } +} diff --git a/packages/providers/s3/src/provider/index.ts b/packages/providers/s3/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/s3/src/provider/index.ts +++ b/packages/providers/s3/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/service-setup/README.md b/packages/providers/service-setup/README.md index e98fc6db..3c885d25 100644 --- a/packages/providers/service-setup/README.md +++ b/packages/providers/service-setup/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) diff --git a/packages/providers/service-setup/package.json b/packages/providers/service-setup/package.json index 839992e1..01ad9653 100644 --- a/packages/providers/service-setup/package.json +++ b/packages/providers/service-setup/package.json @@ -1,59 +1,59 @@ -{ - "name": "@mdf.js/service-setup-provider", - "version": "0.0.1", - "description": "MMS - config Port for Javascript/Typescript", - "keywords": [ - "NodeJS", - "provider", - "MMS", - "config" - ], - "repository": { - "type": "git", - "url": "https://github.com/mytracontrol/mdf.js.git", - "directory": "packages/providers/service-setup" - }, - "license": "MIT", - "author": "Mytra Control S.L.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json", - "check-dependencies": "npm-check", - "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", - "envDoc": "node ../../../.config/envDoc.mjs", - "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/config/licenses.csv --customPath ../../../.config/customFormat.json", - "mutants": "stryker run stryker.conf.js", - "test": "jest --detectOpenHandles --config ./jest.config.js" - }, - "dependencies": { - "@mdf.js/core": "*", - "@mdf.js/crash": "*", - "@mdf.js/doorkeeper": "*", - "@mdf.js/logger": "*", - "@mdf.js/utils": "*", - "dotenv": "^16.4.5", - "glob": "^11.0.0", - "lodash": "^4.17.21", - "toml": "^3.0.0", - "tslib": "^2.7.0", - "uuid": "^10.0.0", - "yaml": "^2.5.1" - }, - "devDependencies": { - "@mdf.js/repo-config": "*", - "@types/debug": "^4.1.8", - "@types/glob": "^8.1.0", - "@types/lodash": "^4.17.10", - "@types/uuid": "^10.0.0" - }, - "engines": { - "node": ">=16.14.2" - }, - "publishConfig": { - "access": "public" - } -} +{ + "name": "@mdf.js/service-setup-provider", + "version": "0.0.1", + "description": "MMS - config Port for Javascript/Typescript", + "keywords": [ + "NodeJS", + "provider", + "MMS", + "config" + ], + "repository": { + "type": "git", + "url": "https://github.com/mytracontrol/mdf.js.git", + "directory": "packages/providers/service-setup" + }, + "license": "MIT", + "author": "Mytra Control S.L.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "yarn clean && tsc -p tsconfig.build.json", + "check-dependencies": "npm-check", + "clean": "rimraf \"{tsconfig.build.tsbuildinfo,dist}\"", + "envDoc": "node ../../../.config/envDoc.mjs", + "licenses": "license-checker --start ./ --production --csv --out ../../../licenses/providers/config/licenses.csv --customPath ../../../.config/customFormat.json", + "mutants": "stryker run stryker.conf.js", + "test": "jest --detectOpenHandles --config ./jest.config.js" + }, + "dependencies": { + "@mdf.js/core": "*", + "@mdf.js/crash": "*", + "@mdf.js/doorkeeper": "*", + "@mdf.js/logger": "*", + "@mdf.js/utils": "*", + "dotenv": "^16.4.7", + "glob": "^11.0.0", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "uuid": "^11.0.3", + "yaml": "^2.6.1" + }, + "devDependencies": { + "@mdf.js/repo-config": "*", + "@types/debug": "^4.1.8", + "@types/glob": "^8.1.0", + "@types/lodash": "^4.17.13" + }, + "engines": { + "node": ">=16.14.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/providers/service-setup/src/provider/index.ts b/packages/providers/service-setup/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/service-setup/src/provider/index.ts +++ b/packages/providers/service-setup/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/socket-client/README.md b/packages/providers/socket-client/README.md index 4a43d0a4..6bf8425e 100644 --- a/packages/providers/socket-client/README.md +++ b/packages/providers/socket-client/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,7 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** diff --git a/packages/providers/socket-client/package.json b/packages/providers/socket-client/package.json index f27118d2..d8bd3bb3 100644 --- a/packages/providers/socket-client/package.json +++ b/packages/providers/socket-client/package.json @@ -36,8 +36,8 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "joi": "^17.13.3", - "socket.io-client": "^4.8.0", - "tslib": "^2.7.0" + "socket.io-client": "^4.8.1", + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*", @@ -45,7 +45,7 @@ }, "optionalDependencies": { "bufferutil": "^4.0.6", - "utf-8-validate": "^6.0.4" + "utf-8-validate": "^6.0.5" }, "engines": { "node": ">=16.14.2" diff --git a/packages/providers/socket-client/src/provider/Port.ts b/packages/providers/socket-client/src/provider/Port.ts index b96f5ca3..315da461 100644 --- a/packages/providers/socket-client/src/provider/Port.ts +++ b/packages/providers/socket-client/src/provider/Port.ts @@ -1,133 +1,133 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { Layer } from '@mdf.js/core'; -import { Crash } from '@mdf.js/crash'; -import { LoggerInstance } from '@mdf.js/logger'; -import { io } from 'socket.io-client'; -import { CONFIG_PROVIDER_BASE_NAME } from '../config'; -import { Client, Config } from './types'; - -interface ExtendedError extends Error { - data?: any; -} -export class Port extends Layer.Provider.Port { - /** Client handler */ - private readonly instance: Client; - /** - * Implementation of functionalities of an HTTP client port instance. - * @param config - Port configuration options - * @param logger - Port logger, to be used internally - */ - constructor(config: Config, logger: LoggerInstance) { - super(config, logger, CONFIG_PROVIDER_BASE_NAME); - this.instance = io(this.config.url as string, { - ...config, - autoConnect: false, - reconnection: config.reconnection ?? true, - }); - // Stryker disable next-line all - this.logger.debug(`New instance of Socket.IO Client port: ${this.uuid}`, this.uuid, this.name); - } - /** Return the underlying port instance */ - public get client(): Client { - return this.instance; - } - /** Return the port state as a boolean value, true if the port is available, false in otherwise */ - public get state(): boolean { - return this.instance.connected; - } - /** Initialize the port instance */ - public start(): Promise { - if (this.instance.connected) { - // Stryker disable next-line all - this.logger.warn(`Port is already connected: ${this.config.host}:${this.config.port}`); - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - const onConnect = () => { - this.instance.removeListener('connect_error', onConnectError); - this.instance.io.removeListener('reconnect_failed', onLastFail); - this.eventsWrapping(this.instance); - resolve(); - }; - this.instance.once('connect', onConnect); - - const onConnectError = (error: ExtendedError) => { - // Stryker disable next-line all - this.logger.error(error.message); - if (error.data && error.data.status) { - onLastFail( - new Crash(`Connection error: ${error.message}, client validation error`, { - info: error.data, - }) - ); - } else if (this.config.reconnection === false) { - onLastFail(new Crash(`Connection error: ${error.message}, no reconnect is configured`)); - } - }; - const onLastFail = (error?: Crash) => { - // Stryker disable next-line all - this.logger.error(`Initial connection error`); - this.instance.removeListener('connect', onConnect); - this.instance.removeListener('connect_error', onConnectError); - this.instance.io.removeListener('reconnect_failed', onLastFail); - reject(error || new Crash('Socket.IO Client connection error')); - }; - this.instance.on('connect_error', onConnectError); - this.instance.io.on('reconnect_failed', onLastFail); - this.instance.connect(); - }); - } - /** Stop the port instance */ - public stop(): Promise { - this.eventsUnwrapping(this.instance); - if (!this.instance.connected) { - // Stryker disable next-line all - this.logger.warn(`Port is not connected: ${this.config.host}:${this.config.port}`); - return Promise.resolve(); - } - return new Promise(resolve => { - const onDisconnect = (reason: string) => { - // Stryker disable next-line all - this.logger.debug(`Port disconnected: ${reason}`); - resolve(); - }; - this.instance.once('disconnect', onDisconnect); - this.instance.close(); - }); - } - /** Close the port instance */ - public async close(): Promise { - await this.stop(); - } - /** Event handler for the `connect` event */ - private readonly onConnectEvent = () => this.emit('healthy'); - /** Event handler for the `disconnect` event */ - private readonly onDisconnectEvent = (reason: string) => { - // Stryker disable next-line all - this.logger.debug(`Port disconnected: ${reason}`); - if (!reason.includes('disconnect')) { - this.emit('unhealthy', new Crash(`Socket.IO Client connection error: ${reason}`)); - } - }; - /** - * Adapts the `client` instance events to standard Port events - * @param instance - Client instance over which the events should be wrapped - */ - private eventsWrapping(instance: Client): void { - instance.on('connect', this.onConnectEvent); - instance.on('disconnect', this.onDisconnectEvent); - } - /** - * Clean all the events handlers - * @param instance - Client instance over which the events should be cleaned - */ - private eventsUnwrapping(instance: Client): void { - instance.off('connect', this.onConnectEvent); - instance.off('disconnect', this.onDisconnectEvent); - } -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { Layer } from '@mdf.js/core'; +import { Crash } from '@mdf.js/crash'; +import { LoggerInstance } from '@mdf.js/logger'; +import { io } from 'socket.io-client'; +import { CONFIG_PROVIDER_BASE_NAME } from '../config'; +import { Client, Config } from './types'; + +interface ExtendedError extends Error { + data?: any; +} +export class Port extends Layer.Provider.Port { + /** Client handler */ + private readonly instance: Client; + /** + * Implementation of functionalities of an HTTP client port instance. + * @param config - Port configuration options + * @param logger - Port logger, to be used internally + */ + constructor(config: Config, logger: LoggerInstance) { + super(config, logger, CONFIG_PROVIDER_BASE_NAME); + this.instance = io(this.config.url as string, { + ...config, + autoConnect: false, + reconnection: config.reconnection ?? true, + }); + // Stryker disable next-line all + this.logger.debug(`New instance of Socket.IO Client port: ${this.uuid}`, this.uuid, this.name); + } + /** Return the underlying port instance */ + public get client(): Client { + return this.instance; + } + /** Return the port state as a boolean value, true if the port is available, false in otherwise */ + public get state(): boolean { + return this.instance.connected; + } + /** Initialize the port instance */ + public start(): Promise { + if (this.instance.connected) { + // Stryker disable next-line all + this.logger.warn(`Port is already connected: ${this.config.host}:${this.config.port}`); + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + const onConnect = () => { + this.instance.removeListener('connect_error', onConnectError); + this.instance.io.removeListener('reconnect_failed', onLastFail); + this.eventsWrapping(this.instance); + resolve(); + }; + this.instance.once('connect', onConnect); + + const onConnectError = (error: ExtendedError) => { + // Stryker disable next-line all + this.logger.error(error.message); + if (error.data?.status) { + onLastFail( + new Crash(`Connection error: ${error.message}, client validation error`, { + info: error.data, + }) + ); + } else if (this.config.reconnection === false) { + onLastFail(new Crash(`Connection error: ${error.message}, no reconnect is configured`)); + } + }; + const onLastFail = (error?: Crash) => { + // Stryker disable next-line all + this.logger.error(`Initial connection error`); + this.instance.removeListener('connect', onConnect); + this.instance.removeListener('connect_error', onConnectError); + this.instance.io.removeListener('reconnect_failed', onLastFail); + reject(error || new Crash('Socket.IO Client connection error')); + }; + this.instance.on('connect_error', onConnectError); + this.instance.io.on('reconnect_failed', onLastFail); + this.instance.connect(); + }); + } + /** Stop the port instance */ + public stop(): Promise { + this.eventsUnwrapping(this.instance); + if (!this.instance.connected) { + // Stryker disable next-line all + this.logger.warn(`Port is not connected: ${this.config.host}:${this.config.port}`); + return Promise.resolve(); + } + return new Promise(resolve => { + const onDisconnect = (reason: string) => { + // Stryker disable next-line all + this.logger.debug(`Port disconnected: ${reason}`); + resolve(); + }; + this.instance.once('disconnect', onDisconnect); + this.instance.close(); + }); + } + /** Close the port instance */ + public async close(): Promise { + await this.stop(); + } + /** Event handler for the `connect` event */ + private readonly onConnectEvent = () => this.emit('healthy'); + /** Event handler for the `disconnect` event */ + private readonly onDisconnectEvent = (reason: string) => { + // Stryker disable next-line all + this.logger.debug(`Port disconnected: ${reason}`); + if (!reason.includes('disconnect')) { + this.emit('unhealthy', new Crash(`Socket.IO Client connection error: ${reason}`)); + } + }; + /** + * Adapts the `client` instance events to standard Port events + * @param instance - Client instance over which the events should be wrapped + */ + private eventsWrapping(instance: Client): void { + instance.on('connect', this.onConnectEvent); + instance.on('disconnect', this.onDisconnectEvent); + } + /** + * Clean all the events handlers + * @param instance - Client instance over which the events should be cleaned + */ + private eventsUnwrapping(instance: Client): void { + instance.off('connect', this.onConnectEvent); + instance.off('disconnect', this.onDisconnectEvent); + } +} diff --git a/packages/providers/socket-client/src/provider/index.ts b/packages/providers/socket-client/src/provider/index.ts index 02c15277..a06cc4c0 100644 --- a/packages/providers/socket-client/src/provider/index.ts +++ b/packages/providers/socket-client/src/provider/index.ts @@ -1,9 +1,11 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export { Factory } from './Factory'; -export { Client, Config, ProviderInstance as Provider } from './types'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { Client, Config, ProviderInstance as Provider } from './types'; + diff --git a/packages/providers/socket-server/README.md b/packages/providers/socket-server/README.md index 34e58664..71bad8a0 100644 --- a/packages/providers/socket-server/README.md +++ b/packages/providers/socket-server/README.md @@ -3,6 +3,7 @@ [![Node Version](https://img.shields.io/static/v1?style=flat\&logo=node.js\&logoColor=green\&label=node\&message=%3E=20\&color=blue)](https://nodejs.org/en/) [![Typescript Version](https://img.shields.io/static/v1?style=flat\&logo=typescript\&label=Typescript\&message=5.4\&color=blue)](https://www.typescriptlang.org/) [![Known Vulnerabilities](https://img.shields.io/static/v1?style=flat\&logo=snyk\&label=Vulnerabilities\&message=0\&color=300A98F)](https://snyk.io/package/npm/snyk) +[![Documentation](https://img.shields.io/static/v1?style=flat\&logo=markdown\&label=Documentation\&message=API\&color=blue)](https://mytracontrol.github.io/mdf.js/) @@ -27,6 +28,7 @@ - [**Installation**](#installation) - [**Information**](#information) - [**Use**](#use) + - [**Environment variables**](#environment-variables) - [**License**](#license) ## **Introduction** diff --git a/packages/providers/socket-server/package.json b/packages/providers/socket-server/package.json index 7bd72561..4d2903a1 100644 --- a/packages/providers/socket-server/package.json +++ b/packages/providers/socket-server/package.json @@ -37,10 +37,10 @@ "@mdf.js/logger": "*", "@mdf.js/utils": "*", "@socket.io/admin-ui": "^0.5.1", - "express": "^4.21.1", + "express": "^4.21.2", "joi": "^17.13.3", - "socket.io": "^4.8.0", - "tslib": "^2.7.0" + "socket.io": "^4.8.1", + "tslib": "^2.8.1" }, "devDependencies": { "@mdf.js/repo-config": "*", @@ -48,7 +48,7 @@ }, "optionalDependencies": { "bufferutil": "^4.0.6", - "utf-8-validate": "^6.0.4" + "utf-8-validate": "^6.0.5" }, "engines": { "node": ">=16.14.2" diff --git a/packages/providers/socket-server/src/provider/index.ts b/packages/providers/socket-server/src/provider/index.ts index 0a7bd2f5..b63df5bf 100644 --- a/packages/providers/socket-server/src/provider/index.ts +++ b/packages/providers/socket-server/src/provider/index.ts @@ -1,14 +1,15 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -import { Layer } from '@mdf.js/core'; -import { Port } from './Port'; -import { Config, Server } from './types'; - -export { Factory } from './Factory'; -export { Config, Server } from './types'; -export type Provider = Layer.Provider.Manager; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +import { Layer } from '@mdf.js/core'; +import { Port } from './Port'; +import { Config, Server } from './types'; + +export { Factory } from './Factory'; +export { Port } from './Port'; +export { BasicAuthentication, Config, ConnectionError, InstrumentOptions, Server } from './types'; +export type Provider = Layer.Provider.Manager; diff --git a/packages/providers/socket-server/src/provider/types/InstrumentOptions.i.ts b/packages/providers/socket-server/src/provider/types/InstrumentOptions.i.ts index cf6ec24e..ff46e241 100644 --- a/packages/providers/socket-server/src/provider/types/InstrumentOptions.i.ts +++ b/packages/providers/socket-server/src/provider/types/InstrumentOptions.i.ts @@ -1,37 +1,39 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ -import { InMemoryStore, RedisStore } from '@socket.io/admin-ui'; - -interface BasicAuthentication { - type: 'basic'; - username: string; - password: string; -} - -export interface InstrumentOptions { - /** - * The name of the admin namespace - * @default "/admin" - */ - namespaceName: string; - /** The authentication method */ - auth?: false | BasicAuthentication; - /** - * Whether updates are allowed - * @default false - */ - readonly: boolean; - /** - * The unique ID of the server - * @default `require("os").hostname()` - */ - serverId?: string; - /** The store */ - store: InMemoryStore | RedisStore; - /** Whether to send all events or only aggregated events to the UI, for performance purposes. */ - mode: 'development' | 'production'; -} +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ +import { InMemoryStore, RedisStore } from '@socket.io/admin-ui'; + +/** The basic authentication method */ +export interface BasicAuthentication { + type: 'basic'; + username: string; + password: string; +} + +/** The instrument options */ +export interface InstrumentOptions { + /** + * The name of the admin namespace + * @defaultValue "/admin" + */ + namespaceName: string; + /** The authentication method */ + auth?: false | BasicAuthentication; + /** + * Whether updates are allowed + * @defaultValue false + */ + readonly: boolean; + /** + * The unique ID of the server + * @defaultValue `require("os").hostname()` + */ + serverId?: string; + /** The store */ + store: InMemoryStore | RedisStore; + /** Whether to send all events or only aggregated events to the UI, for performance purposes. */ + mode: 'development' | 'production'; +} diff --git a/packages/providers/socket-server/src/provider/types/index.ts b/packages/providers/socket-server/src/provider/types/index.ts index d89ae963..067fe940 100644 --- a/packages/providers/socket-server/src/provider/types/index.ts +++ b/packages/providers/socket-server/src/provider/types/index.ts @@ -1,10 +1,12 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -export * from './Client.t'; -export * from './Config.t'; -export * from './ConnectionError.i'; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +export * from './Client.t'; +export * from './Config.t'; +export * from './ConnectionError.i'; +export * from './InstrumentOptions.i'; + diff --git a/packages/tools/repo-config/src/getJestConfig.js b/packages/tools/repo-config/src/getJestConfig.js index ab0ae1ef..f19ade46 100644 --- a/packages/tools/repo-config/src/getJestConfig.js +++ b/packages/tools/repo-config/src/getJestConfig.js @@ -1,32 +1,32 @@ -/** - * Copyright 2024 Mytra Control S.L. All rights reserved. - * - * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file - * or at https://opensource.org/licenses/MIT. - */ - -const config = require('./jest.config'); -const os = require('os'); - -function getJestConfig(modulePath) { - // The split should be different depending on the OS, in case of Windows, it should be '\\' instead - // of '/' for the linux based systems. - const modulePathParts = modulePath.split(os.platform() === 'win32' ? '\\' : '/'); - const internalScope = modulePathParts[modulePathParts.length - 2]; - const packetName = modulePathParts[modulePathParts.length - 1]; - const coverageDirectory = `../../../coverage/${internalScope}/${packetName}`; - const relativeCoverageDirectory = `/${coverageDirectory}`; - return { - ...config, - displayName: packetName, - coverageDirectory: `${relativeCoverageDirectory}`, - reporters: [ - 'default', - ['jest-junit', { outputDirectory: `${relativeCoverageDirectory}`, outputName: `test-results.xml` }], - ['jest-slow-test-reporter', { numTests: 8, warnOnSlowerThan: 300, color: true }], - ['jest-html-reporter', { outputPath: `${relativeCoverageDirectory}/report.html` }], - ['jest-html-reporters', { publicPath: `${relativeCoverageDirectory}`, filename: 'report.html', darkTheme: true}] - ], - }; -} -module.exports = getJestConfig; +/** + * Copyright 2024 Mytra Control S.L. All rights reserved. + * + * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file + * or at https://opensource.org/licenses/MIT. + */ + +const config = require('./jest.config'); +const os = require('os'); + +function getJestConfig(modulePath) { + // The split should be different depending on the OS, in case of Windows, it should be '\\' instead + // of '/' for the linux based systems. + const modulePathParts = modulePath.split(os.platform() === 'win32' ? '\\' : '/'); + const internalScope = modulePathParts[modulePathParts.length - 2]; + const packetName = modulePathParts[modulePathParts.length - 1]; + const coverageDirectory = `../../../coverage/${internalScope}/${packetName}`; + const relativeCoverageDirectory = `/${coverageDirectory}`; + return { + ...config, + displayName: packetName, + coverageDirectory: `${relativeCoverageDirectory}`, + reporters: [ + 'default', + ['jest-junit', { outputDirectory: `${relativeCoverageDirectory}`, outputName: `test-results.xml` }], + ['jest-slow-test-reporter', { numTests: 8, warnOnSlowerThan: 300, color: true }], + ['jest-html-reporter', { outputPath: `${relativeCoverageDirectory}/report.html` }], + ['jest-html-reporters', { publicPath: `${relativeCoverageDirectory}`, filename: 'test.html', darkTheme: true}] + ], + }; +} +module.exports = getJestConfig; diff --git a/test.mjs b/test.mjs deleted file mode 100644 index ec78e11c..00000000 --- a/test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -process.env.DEBUG = 'test:*'; -import { JSONLArchiver } from '@mdf.js/jsonl-archiver-provider'; -import { Logger } from '@mdf.js/logger'; - -const logger = new Logger('test', { - console: { enabled: true, level: 'debug' }, - file: { enabled: true, level: 'debug' }, -}); -const provider = JSONLArchiver.Factory.create({ - config: { - createFolders: true, - rotationSize: 1024 * 1024 * 1, - rotationInterval: 5 * 60 * 1000, - rotationLines: 2000, - propertyFileName: 'file', - }, - logger, -}); - -provider.on('error', error => { - logger.error(error); -}); -provider.client.on('rotate', file => { - logger.info(JSON.stringify(file, null, 2)); -}); - -await provider.start(); -while (true) { - await provider.client.append({ - hello: 'world', - time: new Date().toISOString(), - pid: process.pid, - file: 'a', - }); - await new Promise(resolve => setTimeout(resolve, 100)); - await provider.client.append({ - hello: 'world', - time: new Date().toISOString(), - pid: process.pid, - file: 'b', - }); - await new Promise(resolve => setTimeout(resolve, 100)); - await provider.client.append({ - hello: 'world', - time: new Date().toISOString(), - pid: process.pid, - file: 'c', - }); - await new Promise(resolve => setTimeout(resolve, 100)); -} diff --git a/turbo.json b/turbo.json index b6268cb3..3595ed2f 100644 --- a/turbo.json +++ b/turbo.json @@ -1,31 +1,34 @@ -{ - "$schema": "https://turborepo.org/schema.json", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**"] - }, - "test": { - "dependsOn": ["build"], - "outputs": [], - "inputs": ["src/**/*.ts"] - }, - "mutants": { - "dependsOn": ["test"], - "outputs": [], - "inputs": ["src/**/*.ts"] - }, - "licenses": { - "outputs": [] - }, - "doc": { - "outputs": [] - }, - "envDoc": { - "outputs": [] - }, - "check-dependencies": { - "outputs": [] - } - } -} +{ + "$schema": "https://turborepo.org/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "test": { + "dependsOn": ["build"], + "outputs": [], + "inputs": ["src/**/*.ts"] + }, + "mutants": { + "dependsOn": ["test"], + "outputs": [], + "inputs": ["src/**/*.ts"] + }, + "licenses": { + "outputs": [] + }, + "owasp": { + "outputs": [] + }, + "doc": { + "outputs": [] + }, + "envDoc": { + "outputs": [] + }, + "check-dependencies": { + "outputs": [] + } + } +} diff --git a/typedoc.json b/typedoc.json index 2fde97b7..882abcb8 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,20 +1,23 @@ -{ - "entryPoints": [ - "packages/api/*", - "packages/components/*", - "packages/providers/*", - "packages/registries/*", - ], - "entryPointStrategy": "packages", - "name": "@mdf.js", - "includeVersion": false, - "readme": "README.md", - "plugin": [], - "navigation": { - "includeCategories": false, - "includeGroups": false, - "includeFolders": false - }, - "categorizeByGroup": true, - "logLevel": "Info", -} +{ + "entryPoints": [ + "packages/api/*", + "packages/components/*", + "packages/providers/*", + ], + "entryPointStrategy": "packages", + "name": "@mdf.js", + "includeVersion": false, + "readme": "README.md", + "plugin": [ + "typedoc-plugin-missing-exports", + ], + "navigation": { + "includeCategories": false, + "includeGroups": false, + "includeFolders": false + }, + "categorizeByGroup": true, + "useTsLinkResolution" : true, + "logLevel": "Info", + "hideGenerator": true +}