diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2db98af --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# IntelliJ Settings +.idea + +# VSCode Settings +.vscode \ No newline at end of file diff --git a/cdsp/information-layer/.dockerignore b/cdsp/information-layer/.dockerignore new file mode 100644 index 0000000..1c412eb --- /dev/null +++ b/cdsp/information-layer/.dockerignore @@ -0,0 +1,6 @@ +# ignore mongodb generated files +mongodb-realm + +# ignore all node generated file +node_modules +package-lock.json \ No newline at end of file diff --git a/cdsp/information-layer/.gitignore b/cdsp/information-layer/.gitignore new file mode 100644 index 0000000..060b123 --- /dev/null +++ b/cdsp/information-layer/.gitignore @@ -0,0 +1,8 @@ +# MongoDB Realm +mongodb-realm/ + +# Node.js +node_modules/ + +# typescript migration +dist \ No newline at end of file diff --git a/cdsp/information-layer/.prettierignore b/cdsp/information-layer/.prettierignore new file mode 100644 index 0000000..3e52534 --- /dev/null +++ b/cdsp/information-layer/.prettierignore @@ -0,0 +1,5 @@ +#ignore node modules +node_modules + +#ignore autogenerated files by thrift +handlers/src/iotdb/gen-nodejs \ No newline at end of file diff --git a/cdsp/information-layer/Dockerfile b/cdsp/information-layer/Dockerfile new file mode 100644 index 0000000..349690d --- /dev/null +++ b/cdsp/information-layer/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22.5.1 + +WORKDIR /app + +COPY package.json . +COPY handlers ./handlers +COPY utils ./utils +COPY router ./router + +# Install dependencies, including workspaces +RUN npm install --production + +# Expose the necessary port +EXPOSE 8080 + +# Command to run the WebSocket server +CMD ["node", "router/src/websocket-server.js"] diff --git a/cdsp/information-layer/README.md b/cdsp/information-layer/README.md new file mode 100644 index 0000000..b117028 --- /dev/null +++ b/cdsp/information-layer/README.md @@ -0,0 +1,214 @@ +# Information Layer + +The information layer is responsible for providing a raw data access [API](#api) (read, write, subscribe). +Its intention is to abstract the underlying data storage database technology. + +Clients can interact with it using websockets and JSON payload. + + +The information-layer consists of two logical components: +- database handler: Interaction with the chosen database, configurable +- router: API provider that connects to the database handler +``` + | CLOUD + +---------+ | +---------+ + | realm_h | <-> | realmDB | ++--------+ +--------+ +----------+ <-> +---------+ | +---------+ +| client | <-> | router | <-> | handlers | ++--------+ +--------+ +----------+ <-> +---------+ +-------+ + | iotdb_h | <-> | iotdb | + +---------+ +-------+ +``` + +# Hello World Setup + +## Backend Option 1: IoTDB + +### Start the database + +Start the db: +```bash +docker run -d --rm --name iotdb -p 6667:6667 -p 9003:9003 apache/iotdb:latest +``` +Connect to it via cli: +```bash +docker exec -it iotdb ./start-cli.sh -h 127.0.0.1 -p 6667 -u root -pw root +``` +Create a database +```bash +create database root.Vehicles +``` +Create desired timeseries + +```bash +create timeseries root.Vehicles.Vehicle_TraveledDistance WITH DATATYPE=FLOAT, ENCODING=RLE +create timeseries root.Vehicles.Vehicle_Speed WITH DATATYPE=FLOAT, ENCODING=RLE +``` + +### Start router + +Build the router image +```bash +docker build -t router . +``` +Run the router +```bash +# Docker +docker run --rm --name router -p 8080:8080 -e HANDLER_TYPE=iotdb -e IOTDB_HOST=host.docker.internal router +# OR natively +npm install +HANDLER_TYPE=iotdb IOTDB_HOST=localhost npm start +``` + +## Backend Option 2: RealmDB + + +### Prepare cloud + +- Ensure that in your [ATLAS cloud](https://cloud.mongodb.com/) app there is a vehicle _document_ with an `Vehicle_VehicleIdentification_VIN` in a collection named _`Vehicles`_. +- Ensure that this document as well contains VSS data. Here you can see the data supported in this repository for a vehicle document within _Vehicles_ that should be reflected in ATLAS: + +``` +_id: "" (String) +Vehicle_Speed: (Double) +Vehicle_TraveledDistance: "" (Double) +``` + +### Start router + +Build the router image +```bash +docker build -t router . +``` +Run the router +```bash +# Docker +docker run --rm --name router -p 8080:8080 -e HANDLER_TYPE=realmdb -e VERSION_REALMDB_SCHEMA=0 -e REALM_APP_ID= -e REALM_API_KEY= router +# OR natively +npm install +HANDLER_TYPE=realmdb VERSION_REALMDB_SCHEMA=0 REALM_APP_ID= REALM_API_KEY= npm start +``` + +## Usage + +See [api](#api) how to interact with the router. + +# API + +Connect your own websocket client by connecting to `ws://localhost:8080`. + +The examples use [websocat](https://github.com/vi/websocat) and [jq](https://github.com/jqlang/jq) + +## Read + +Schema: +```jsonc +{ + "type": "read", + "tree": "VSS", + "id": "123", // The VIN + "uuid": "testClient", // The unique client ID + // For reading one + "node": { "name": "Vehicle_Speed" }, + // For reading N + "nodes": [{ "name": "Vehicle_Speed" },{ "name": "Vehicle_TraveledDistance" }] +} +``` + +Example: +```bash +echo '{ "type": "read", "tree": "VSS", "id": "123", "uuid": "testClient", "node": { "name": "Vehicle.Speed" } }' | websocat ws://localhost:8080 | jq +``` +```json +{ + "type": "update", + "tree": "VSS", + "id": "123", + "dateTime": "2024-10-16T12:09:13.084Z", + "uuid": "test", + "node": { + "name": "Vehicle.Speed", + "value": 300 + } +} +``` + +## Write + +Schema: +```jsonc +{ + "type": "write", + "tree": "VSS", + "id": "123", // The VIN + "uuid": "testClient", // The unique client ID + // For writing one + "node": { "name": "Vehicle_Speed", "value": 300 }, + // For writing N + "nodes": [{ "name": "Vehicle_Speed", "value": 300 },{ "name": "Vehicle_TraveledDistance", "value": 100000 }] +} +``` +Example: +```bash +echo '{ "type": "write", "tree": "VSS", "id": "123", "uuid": "testClient", "node": { "name": "Vehicle.Speed", "value": 300 } }' | websocat ws://localhost:8080 | jq +``` +```json +{ + "type": "update", + "tree": "VSS", + "id": "123", + "dateTime": "2024-10-16T12:09:13.084Z", + "uuid": "test", + "node": { + "name": "Vehicle.Speed", + "value": 300 + } +} +``` + +## Subscribe (Realm Only) + +Schema: +```json +{ + "type": "subscribe", + "tree": "VSS", + "id": "123", + "uuid": "testClient" +} +``` + +On success: +```json +{ + "type": "subscribe:status", + "tree": "VSS", + "id": "123", + "dateTime": "2024-09-12T15:50:17.232Z", + "uuid": "testClient", + "status": "succeed" +} +``` +## Unsubscribe + +```json +{ + "type": "unsubscribe", + "tree": "VSS", + "id": "123", + "uuid": "testClient" +} +``` + +On success: +```json +{ + "type": "unsubscribe:status", + "tree": "VSS", + "id": "123", + "dateTime": "2024-09-12T17:40:00.754Z", + "uuid": "testClient", + "status": "succeed" +} +``` + diff --git a/cdsp/information-layer/eslint.config.mjs b/cdsp/information-layer/eslint.config.mjs new file mode 100644 index 0000000..0ee2f92 --- /dev/null +++ b/cdsp/information-layer/eslint.config.mjs @@ -0,0 +1,38 @@ +// @ts-check + +import eslint from "@eslint/js"; +import pluginPrettier from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; +import tseslint, { parser } from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["coverage/**", "**/dist/**", "node_modules/**"], + }, + { + files: ["./**/*.{ts,tsx}"], + extends: [eslint.configs.recommended, ...tseslint.configs.strict], + plugins: { + prettier: pluginPrettier, + //"@typescript-eslint": ESLintPlugin, + }, + languageOptions: { + parser: parser, + parserOptions: { + project: "./tsconfig.json", // Ensure it points to your tsconfig + }, + }, + rules: { + "max-depth": ["error", 3], + "@/no-console": "error", + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-floating-promises": "error", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/naming-convention": "error", + "@no-nested-ternary": "warn", + "prettier/prettier": "error", + ...prettierConfig.rules, + }, + } +); diff --git a/cdsp/information-layer/handlers/config/README.md b/cdsp/information-layer/handlers/config/README.md new file mode 100644 index 0000000..8cbbd5c --- /dev/null +++ b/cdsp/information-layer/handlers/config/README.md @@ -0,0 +1,18 @@ +# Configuration Handler for supported Data Points + +This module is responsible for handling the configuration of supported data points in the application. It supports JSON, YAML, or YML formats for defining the data points. + +The `vss_data_points.yaml` and `vss_data_points.json` files in the `./schema-files` directory contain the same data point definition (only one of them is necessary to build) and based on that, the system will configure the data schema used during the Database configuration. + +> [!WARNING] +> +> - Before starting the application, ensure that the desired YAML, YML or JSON file is correctly placed in the `./schema-files` directory. This file should contain the definitions of the supported data points. The name and extension to use can be configured in the `/docker/.env` file, it uses the ENV variable named `DATA_POINTS_SCHEMA_FILE`. If it is not specified, it uses `vss_data_points.yaml` by default, which is one of the files contained in this repository. . +> - Ensure that the used file is correctly formatted and contains valid data points definitions. +> - **The application will not function correctly if the data points file is missing or incorrectly placed.** + +## File: `config.js` + +### Description + +The `config.js` file contains the logic to retrieve the data defined in the ENV variables and the full path to the data points schema configuration file (`./schema-files/vss_data_points.yaml` in this case). This file is crucial for the application to understand which data points are supported and how they should be handled. + diff --git a/cdsp/information-layer/handlers/config/config.ts b/cdsp/information-layer/handlers/config/config.ts new file mode 100644 index 0000000..6cd2c9d --- /dev/null +++ b/cdsp/information-layer/handlers/config/config.ts @@ -0,0 +1,39 @@ +import path from "path"; +import dotenv from "dotenv"; +import { logMessage } from "../../utils/logger"; + +// Load environment variables from the .env file +dotenv.config(); + +/** + * This file contains the description of the supported data points. + * It supports JSON, YAML or YML format. + */ +const ENDPOINTS_FILE: string = "vss_data_points.yaml"; + +/** + * Retrieves the value of an environment variable. + * + * @param envVar - The environment variable to retrieve. + * @returns The value of the environment variable or null if not set. + */ +export const getEnvValue = (envVar: string): string | null => { + if (!process.env[envVar]) { + logMessage(`${envVar} environment variable is not set in .env file.`); + return null; + } + return process.env[envVar]!; +}; + +/** + * Retrieves the full path to the data points file. + * + * This function resolves the schema-files directory path of the current module + * and joins it with the ENDPOINTS_FILE constant to form the full path. + * + * @returns The full path to the data points file. + */ +export const getDataPointsPath = (): string => { + const rootPath = path.resolve(`${__dirname}/schema-files`); + return path.join(rootPath, ENDPOINTS_FILE); +}; diff --git a/cdsp/information-layer/handlers/config/schema-files/vss_data_points.yaml b/cdsp/information-layer/handlers/config/schema-files/vss_data_points.yaml new file mode 100644 index 0000000..625c731 --- /dev/null +++ b/cdsp/information-layer/handlers/config/schema-files/vss_data_points.yaml @@ -0,0 +1,87 @@ +Vehicle: + description: High-level vehicle data. + type: branch + +Vehicle.Chassis: + description: All data concerning steering, suspension, wheels, and brakes. + type: branch + +Vehicle.Chassis.SteeringWheel: + description: Steering wheel signals + type: branch + +Vehicle.Chassis.SteeringWheel.Angle: + datatype: int16 + description: Steering wheel angle. Positive = degrees to the left. Negative = degrees to the right. + type: sensor + unit: degrees + +Vehicle.CurrentLocation: + description: The current latitude and longitude of the vehicle. + type: branch + +Vehicle.CurrentLocation.Latitude: + datatype: double + description: Current latitude of vehicle in WGS 84 geodetic coordinates, as measured at the position of GNSS receiver antenna. + max: 90 + min: -90 + type: sensor + unit: degrees + +Vehicle.CurrentLocation.Longitude: + datatype: double + description: Current longitude of vehicle in WGS 84 geodetic coordinates, as measured at the position of GNSS receiver antenna. + max: 180 + min: -180 + type: sensor + unit: degrees + +Vehicle.Powertrain: + description: Powertrain data for battery management, etc. + type: branch + +Vehicle.Powertrain.TractionBattery: + description: Battery Management data. + type: branch + +Vehicle.Powertrain.TractionBattery.NominalVoltage: + comment: Nominal voltage typically refers to voltage of fully charged battery when delivering rated capacity. + datatype: uint16 + description: Nominal Voltage of the battery. + type: attribute + unit: V + +Vehicle.Powertrain.TractionBattery.StateOfCharge: + description: Information on the state of charge of the vehicle's high voltage battery. + type: branch + +Vehicle.Powertrain.TractionBattery.StateOfCharge.CurrentEnergy: + comment: Current energy could be calculated as .StateOfCharge.Current * .NetCapacity. + datatype: float + description: Physical state of charge of high voltage battery expressed in kWh. + type: sensor + unit: kWh + +Vehicle.Powertrain.Transmission: + description: Transmission-specific data, stopping at the drive shafts. + type: branch + +Vehicle.Powertrain.Transmission.CurrentGear: + datatype: int8 + description: The current gear. 0=Neutral, 1/2/..=Forward, -1/-2/..=Reverse. + type: sensor + +Vehicle.Speed: + datatype: float + description: Vehicle speed. + type: sensor + unit: km/h + +Vehicle.VehicleIdentification: + description: Attributes that identify a vehicle. + type: branch + +Vehicle.VehicleIdentification.VIN: + datatype: string + description: 17-character Vehicle Identification Number (VIN) as defined by ISO 3779. + type: attribute diff --git a/cdsp/information-layer/handlers/src/HandlerBase.ts b/cdsp/information-layer/handlers/src/HandlerBase.ts new file mode 100644 index 0000000..4b7b00b --- /dev/null +++ b/cdsp/information-layer/handlers/src/HandlerBase.ts @@ -0,0 +1,235 @@ +import fs from "fs"; +import yaml from "js-yaml"; +import { + Message, + WebSocket, + DataPointSchema, + MessageBase, + ErrorMessage, +} from "../utils/data_types"; +import { getDataPointsPath } from "../config/config"; +import { logMessage, MessageType } from "../../utils/logger"; + +export abstract class HandlerBase { + // Default implementations of required functions + authenticateAndConnect(sendMessageToClients: (message: any) => void): void { + logMessage( + "authenticateAndConnect() is not implemented", + MessageType.WARNING + ); + } + + protected read(message: Message, ws: WebSocket): void { + logMessage("read() is not implemented", MessageType.WARNING); + } + + protected write(message: Message, ws: WebSocket): void { + logMessage("write() is not implemented", MessageType.WARNING); + } + + protected subscribe(message: Message, ws: WebSocket): void { + logMessage("subscribe() is not implemented", MessageType.WARNING); + } + + protected unsubscribe(message: Message, ws: WebSocket): void { + logMessage("unsubscribe() is not implemented", MessageType.WARNING); + } + + unsubscribe_client(ws: WebSocket): void { + logMessage("unsubscribe_client() is not implemented", MessageType.WARNING); + } + + handleMessage(message: Message, ws: WebSocket): void { + try { + switch (message.type) { + case "read": + this.read(message, ws); + break; + case "write": + this.write(message, ws); + break; + case "subscribe": + this.subscribe(message, ws); + break; + case "unsubscribe": + this.unsubscribe(message, ws); + break; + default: + ws.send(JSON.stringify({ error: "Unknown message type" })); + } + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + ws.send(errMsg); + } + } + + /** + * Sends a message to the client. + */ + protected sendMessageToClient( + ws: WebSocket, + message: Message | MessageBase | ErrorMessage + ): void { + logMessage(JSON.stringify(message), MessageType.SENT); + ws.send(JSON.stringify(message)); + } + + /** + * Generic function to create an update message. + * @param message - The original message from client. + * @param nodes - The nodes to be included in the message. + * @returns - The transformed message. + */ + protected createUpdateMessage( + message: Pick, + nodes: Array<{ name: string; value: any }> + ): Message { + const { id, tree, uuid } = message; + return { + type: "update", + tree, + id, + dateTime: new Date().toISOString(), + uuid, + ...(nodes.length === 1 + ? { node: nodes[0] } // Return single node as 'node' + : { nodes }), // Return array as 'nodes' } as Message; + }; + } + + /** + * Generic function to create or remove a subscription message. + * @param type - Type of subscription message. + * @param message - The original message from client. + * @param status - The status of the subscription. + * @returns - The transformed message. + */ + protected createSubscribeMessage( + type: "subscribe" | "unsubscribe", + message: Pick, + status: string + ): MessageBase { + const { id, tree, uuid } = message; + return { + type: `${type}:status` as MessageBase["type"], + tree, + id, + dateTime: new Date().toISOString(), + uuid, + status: status, + }; + } + + /** + * Transforms a message node by replacing dots with underscores. + * @param node - The message node to transform. + * @returns - The transformed message node with dots replaced by underscores. + */ + protected transformDataPointsWithUnderscores(node: string): string { + return node.replace(/\./g, "_"); + } + + /** + * Transforms a database field name by replacing underscores with dots. + * @param field - The database filed to transform. + * @returns - The transformed to message node replacing underscores by dots. + */ + protected transformDataPointsWithDots(field: string): string { + return field.replace(/\_/g, "."); + } + + /** + * Reads and parses a data points file in either JSON, YML, or YAML format. + */ + private readDataPointsFile(filePath: string): object { + const fileContent = fs.readFileSync(filePath, "utf8"); + const filePathLower = filePath.toLowerCase(); + if (filePathLower.endsWith(".json")) { + return JSON.parse(fileContent); + } else if ( + filePathLower.endsWith(".yaml") || + filePathLower.endsWith(".yml") + ) { + const result = yaml.load(fileContent); + if (typeof result === "object" && result !== null) { + return result; + } else { + throw new Error("YAML content is not a valid object"); + } + } else { + throw new Error("Unsupported data points file format"); + } + } + + /** + * Extracts data types from a data point object. + */ + private extractDataTypes( + dataPointsObj: any, + parentKey = "", + result: { [key: string]: any } = {} + ): { [key: string]: any } { + for (const key in dataPointsObj) { + if (dataPointsObj.hasOwnProperty(key)) { + const value = dataPointsObj[key]; + const newKey = parentKey ? `${parentKey}.${key}` : key; + const isObject = value && typeof value === "object"; + if (isObject && value.datatype) { + result[newKey] = value.datatype; + } else if (isObject) { + this.extractDataTypes(value.children || value, newKey, result); + } + } + } + return result; + } + + /** + * Retrieves and processes supported data points. + * This method reads the data points configuration file, extracts the data types, + * and transforms the data point names to use underscores. It returns an object + * with the transformed data point names as keys and their corresponding data types. + * @returns An object containing the supported data points with transformed names and data types. + */ + protected getSupportedDataPoints(): object { + const dataPointPath = getDataPointsPath(); + const dataPointObj = this.readDataPointsFile(dataPointPath); + const supportedDataPoints = this.extractDataTypes(dataPointObj); + const result: { [key: string]: any } = {}; + Object.entries(supportedDataPoints).forEach(([node, value]) => { + const underscored_node = this.transformDataPointsWithUnderscores(node); + if (value !== null) { + result[underscored_node] = value; + } + }); + return result; + } + + /** + * Validates nodes against a given schema. + * + * @param message - The message containing nodes to be validated. + * @param dataPointsSchema - The schema against which nodes are validated. + * @returns An object containing error details if validation fails, otherwise null. + */ + protected validateNodesAgainstSchema( + message: Message, + dataPointsSchema: DataPointSchema + ): object | null { + const nodes = message.node ? [message.node] : message.nodes || []; + + const unknownFields = nodes.filter(({ name }) => { + const transformedName = this.transformDataPointsWithUnderscores(name); + return !dataPointsSchema.hasOwnProperty(transformedName); + }); + + if (unknownFields.length > 0) { + const errors = unknownFields.map(({ name }) => ({ + name, + status: "Parent object or node not found.", + })); + return errors.length === 1 ? { node: errors[0] } : { nodes: errors }; + } + return null; + } +} diff --git a/cdsp/information-layer/handlers/src/README.md b/cdsp/information-layer/handlers/src/README.md new file mode 100644 index 0000000..56ad64a --- /dev/null +++ b/cdsp/information-layer/handlers/src/README.md @@ -0,0 +1,108 @@ +# Database handlers + +This project already contains handlers configured to be use with RealmDB and IoTDB. + +## Adding a New Database Handler + +This project uses a handler interface to dynamically integrate new database backends such as RealmDB or IoTDB. Each handler must implement the core functionality to handle WebSocket messages (read, write, subscribe, unsubscribe). + +### How to Add a New Database Handler + +1. **Create a new handler class**: + Create a new file for your database handler (e.g., `mydb-handler.js`) in the `./mydb/src` directory. This handler should extend the base `Handler` class from [handler.js](./handler.js). + +2. **Implement the handler methods**: + You must implement the following methods in your new handler: + - `authenticateAndConnect(sendMessageToClients)`: Establish a connection with the database and authenticate. + - `read(message, ws)`: Retrieve data from the database based on the incoming WebSocket message. + - `write(message, ws)`: Write data to the database. + - `subscribe(message, ws)`: Subscribe to changes in the database, and automatically send updates over WebSocket. + - `unsubscribe(message, ws)`: Unsubscribe from database updates. + +3. **Example Handler Implementation**: + Here’s a basic template you can follow: +```js +const Handler = require('../../handler'); + +class MyDBHandler extends Handler { +async authenticateAndConnect(sendMessageToClients) { + // Connect to your database here +} + +async read(message, ws) { + // Implement the logic to read data from the database +} + +async write(message, ws) { + // Implement the logic to write data to the database +} + +async subscribe(message, ws) { + // Implement the logic to subscribe to updates from the database +} + +async unsubscribe(message, ws) { + // Implement the logic to unsubscribe from updates +} +} + +module.exports = MyDBHandler; +``` + +4. **Create configuration files**: + Create the the configuration files into `./mydb/config` to include parameters for your new database (e.g., database names, data schemas, etc.). +> [!IMPORTANT] +> Ensure to create the necessary files to support the necessary data points that will be store in the DB and required for your clients. See [how](../config/README.md). + +5. **Work with the Handler**: + + Create (if it does not exist) `/docker/.env` and add the following environment variables, replacing the values with yours: + +```sh +######################### +# GENERAL CONFIGURATION # +######################### + +# HANDLER_TYPE define the database to initialize +HANDLER_TYPE=mydb +# DATA_POINTS_SCHEMA_FILE is the YAML or JSON file containing all data points supported. See the ../../config/README.md for more information. +DATA_POINTS_SCHEMA_FILE=vss_data_points.yaml +######################### +# MYDB CONFIGURATION # +######################### + +# Other variables are optional, they will not be committed. You can define custom variables like API Keys or secrets. +OPTIONAL_CUSTOM_VARIABLES="value" +``` + +> [!WARNING] +> Do not commit this file to GitHub! + +In order to work with your custom database handler, it is required to initialize it in the [websocket-server.ts](../../router/src/websocket-server.ts). + +```ts +switch (handlerType) { + case "realmdb": + handler = new RealmDBHandler(); + break; + case "iotdb": + handler = new IoTDBHandler(); + break; + // define the new MyDBhandler object. + case "mydb": + handler = new MyDBHandler(); + break; + default: + throw new Error("Unsupported handler type"); +} +``` + +Run the WebSocket server, and connect with your handler by sending WebSocket messages to test reading, writing, and subscribing functionalities. The handler should be started by the DB-Router like described [here](../../README.md). + +### Existing Handlers + +You can check the following examples to understand how to structure your new handler: +- **RealmDB Handler**: [RealmHandler](./realmdb/src/realmdb-handler.ts) provides an example of how to interact with RealmDB. +- **IoTDB Handler**: A similar implementation can be followed for [IoTDBHandler](./iotdb/src/iotdb-handler.ts). + +For additional logging, you can utilize the `logMessage` function from [logger.ts](../../utils/logger.ts). \ No newline at end of file diff --git a/cdsp/information-layer/handlers/src/iotdb/.gitignore b/cdsp/information-layer/handlers/src/iotdb/.gitignore new file mode 100644 index 0000000..71aa67b --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/.gitignore @@ -0,0 +1,2 @@ +# MAC +**/.DS_Store diff --git a/cdsp/information-layer/handlers/src/iotdb/README.md b/cdsp/information-layer/handlers/src/iotdb/README.md new file mode 100644 index 0000000..88dc13f --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/README.md @@ -0,0 +1,48 @@ +# IoTDB + +This directory contains the IoTDB Handler as a Node.js application. As [Apache IoTDB](https://iotdb.apache.org/) is a time-series database, the IoTDB Handler connects to an existing IoTDB instance using the Thrift protocol. The handler utilizes the [IoTDB Thrift API](https://github.com/apache/thrift) to communicate with the database and perform operations. Configuration details, such as the IoTDB host, port, user credentials, and time zone, are specified in the handler's configuration file. The IoTDB Handler is designed to manage sessions, execute queries, and interact with the IoTDB instance efficiently during runtime. + +# Features + +- **Authentication**: Authenticates with IoTDB using the IoTDB host, port, user credentials, number of rows to be fetched, and time zone. +- **Read Data**: Retrieves data from the IoTDB using a VIN as object ID. +- **Write Data**: Write data to the IoTDB using a VIN as object ID. +- **Error Handling**: Logs and handles errors during database operations and synchronization. + +# Configure IoTDB + +Before the Database-Router can start the IoTDB Handler without any errors you need to start and run Docker containers defined in a [Docker Compose file](/docker/). + +## Configure of a IoTDB Handler + +Create (if it does not exist) `/docker/.env` and add the following environment variables, replacing the values with yours. + +```shell + ######################### + # GENERAL CONFIGURATION # + ######################### + + # HANDLER_TYPE define the database to initialize + HANDLER_TYPE=iotdb + # DATA_POINTS_SCHEMA_FILE is the YAML or JSON file containing all data points supported. See the ../../config/README.md for more information. + DATA_POINTS_SCHEMA_FILE=vss_data_points.yaml + + ####################### + # IOTDB CONFIGURATION # + ####################### + + # Access to iotdb-service. All this are optional, they have an predefine default value + IOTDB_HOST="your-iotdb-host" # Docker container name for IoTDB or host, default container name "iotdb-service" + IOTDB_PORT=6667 # Set this to the appropriate IotDB Port, default "6667" + IOTDB_USER="your-iotdb-user" # Default "root" + IOTDB_PASSWORD="your-iotdb-password" # Default "root" + IOTDB_TIMEZONE="your-time-zone" # Default your local configured time zone + FETCH_SIZE=10000 #number of rows that will be fetched from the database at a time when executing a query, default 10000 +``` + +> [!WARNING] +> Do not commit this file to GitHub! + +## Starting the IoTDB handler + +You do not need to start IotDB Handler manually. It is started by the Websocket-Server like described [here](../../../README.md). diff --git a/cdsp/information-layer/handlers/src/iotdb/config/database-params.ts b/cdsp/information-layer/handlers/src/iotdb/config/database-params.ts new file mode 100644 index 0000000..22f8e90 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/config/database-params.ts @@ -0,0 +1,54 @@ +import { getEnvValue } from "../../../config/config"; +import { DatabaseParamsRecord } from "../../../utils/data_types"; + +/** + * Defines the shape of the IoTDB configuration object. + */ +interface IotDBConfig { + iotdbHost: string; + iotdbPort: number; + iotdbUser: string; + iotdbPassword: string; + fetchSize: number; + timeZoneId: string; +} + +/* + * Contains the definition of the database name and its identifier data point for each catalog. + */ +export const databaseParams: Readonly = { + VSS: { + databaseName: "root.Vehicles", // name of the configured IoTDB for the VSS database + dataPointId: "Vehicle_VehicleIdentification_VIN", // data point used as element ID + }, +}; + +/** + * Retrieves the database configuration for IoTDB. + * + * This function gathers configuration values for connecting to an IoTDB instance. + * It first attempts to get the values from environment variables. If the environment + * variables are not set, it uses default values. + * + * @returns {IotDBConfig} An object containing the IoTDB configuration. + */ +const getDatabaseConfig = (): Readonly => { + const iotdb_config: IotDBConfig = { + iotdbHost: getEnvValue("IOTDB_HOST") || "iotdb-service", + iotdbPort: Number(getEnvValue("IOTDB_PORT")) || 6667, + iotdbUser: getEnvValue("IOTDB_USER") || "root", + iotdbPassword: getEnvValue("IOTDB_PASSWORD") || "root", + fetchSize: Number(getEnvValue("IOTDB_FETCH_SIZE")) || 10000, + timeZoneId: + getEnvValue("IOTDB_TIME_ZONE_ID") || + Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + + return iotdb_config; +}; + +/** + * If the HANDLER_TYPE is set to 'iotdb', the IoTDB configuration is used. + */ +export const databaseConfig: Readonly | undefined = + getEnvValue("HANDLER_TYPE") === "iotdb" ? getDatabaseConfig() : undefined; diff --git a/cdsp/information-layer/handlers/src/iotdb/config/iotdb-config.ts b/cdsp/information-layer/handlers/src/iotdb/config/iotdb-config.ts new file mode 100644 index 0000000..a52572e --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/config/iotdb-config.ts @@ -0,0 +1,62 @@ +import { SupportedMessageDataTypes } from "../utils/iotdb-constants"; + +export interface SupportedDataPoints { + [key: string]: keyof typeof SupportedMessageDataTypes; +} + +/** + * Validates and creates the data points schema. + * Ensures only supported data types are included. + * + * @param supportedDataPoints - An object containing the supported data points. + * @returns The validated and immutable data points schema. + */ +function buildSchema( + supportedDataPoints: SupportedDataPoints, +): SupportedDataPoints { + const properties: SupportedDataPoints = {}; + + Object.entries(supportedDataPoints).forEach(([key, value]) => { + if (isSupportedDataPoint(value)) { + properties[key] = value; + } else { + throw new Error( + `The initialized data points contains an unsupported data type: ${value}`, + ); + } + }); + + return Object.freeze(properties); +} +/** + * Type guard to check if a data type is supported by SupportedMessageDataTypes. + * + * @param type - The data type to check. + * @returns A boolean indicating whether the type is valid. + */ +export function isSupportedDataPoint( + type: string, +): type is keyof typeof SupportedMessageDataTypes { + return type in SupportedMessageDataTypes; +} + +// Singleton instance holder +let dataPointsSchemaInstance: SupportedDataPoints | null = null; + +/** + * Creates and returns a singleton instance of DataPointsSchema. + * If the instance does not already exist, it initializes it with the provided supported data points + * and freezes the instance to prevent further modifications. + * + * @param supportedEndpoints - An object of supported data points to initialize the schema. + * @returns The data points schema instance. + */ +export function createDataPointsSchema( + supportedEndpoints: SupportedDataPoints, +): SupportedDataPoints { + if (!dataPointsSchemaInstance) { + dataPointsSchemaInstance = buildSchema(supportedEndpoints); + } + + return dataPointsSchemaInstance; +} diff --git a/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.d.ts b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.d.ts new file mode 100644 index 0000000..d8d786a --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.d.ts @@ -0,0 +1,33 @@ +import * as IClientRPCService from './IClientRPCService_types'; +import { + TSOpenSessionReq, + TSOpenSessionResp, + TSCloseSessionReq, + TSExecuteStatementReq, + TSExecuteStatementResp, + TSInsertRecordReq, +} from './IClientRPCService_types'; +type Callback = (err: E, resp: T) => void; + +interface Int64 { + constructor(o?: number | string): this; + toString(): string; + toJson(): string; +} + +export class Client { + openSession(req: TSOpenSessionReq, callback: Callback): void; + openSession(req: TSOpenSessionReq): Promise; + + closeSession(req: TSCloseSessionReq, callback: Callback): void; + closeSession(req: TSCloseSessionReq): Promise; + + executeQueryStatement(req: TSExecuteStatementReq, callback: Callback): void; + executeQueryStatement(req: TSExecuteStatementReq): Promise; + + requestStatementId(sessionId: Int64, callback: Callback): void; + requestStatementId(sessionId: Int64): Promise; + + insertRecord(req: TSInsertRecordReq, callback: Callback): void; + insertRecord(req: TSInsertRecordReq): Promise; +} diff --git a/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.js b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.js new file mode 100644 index 0000000..86724e1 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.js @@ -0,0 +1,1000 @@ +// +// Autogenerated by Thrift Compiler (0.22.0) +// +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +// +"use strict"; + +var thrift = require('thrift'); +var Thrift = thrift.Thrift; +var Q = thrift.Q; +var Int64 = require('node-int64'); + + +var ttypes = require('./IClientRPCService_types'); +//HELPER FUNCTIONS AND STRUCTURES + +var IClientRPCService_openSession_args = function(args) { + this.req = null; + if (args) { + if (args.req !== undefined && args.req !== null) { + this.req = new ttypes.TSOpenSessionReq(args.req); + } + } +}; +IClientRPCService_openSession_args.prototype = {}; +IClientRPCService_openSession_args.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.STRUCT) { + this.req = new ttypes.TSOpenSessionReq(); + this.req[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_openSession_args.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_openSession_args'); + if (this.req !== null && this.req !== undefined) { + output.writeFieldBegin('req', Thrift.Type.STRUCT, 1); + this.req[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_openSession_result = function(args) { + this.success = null; + if (args) { + if (args.success !== undefined && args.success !== null) { + this.success = new ttypes.TSOpenSessionResp(args.success); + } + } +}; +IClientRPCService_openSession_result.prototype = {}; +IClientRPCService_openSession_result.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 0: + if (ftype == Thrift.Type.STRUCT) { + this.success = new ttypes.TSOpenSessionResp(); + this.success[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_openSession_result.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_openSession_result'); + if (this.success !== null && this.success !== undefined) { + output.writeFieldBegin('success', Thrift.Type.STRUCT, 0); + this.success[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_closeSession_args = function(args) { + this.req = null; + if (args) { + if (args.req !== undefined && args.req !== null) { + this.req = new ttypes.TSCloseSessionReq(args.req); + } + } +}; +IClientRPCService_closeSession_args.prototype = {}; +IClientRPCService_closeSession_args.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.STRUCT) { + this.req = new ttypes.TSCloseSessionReq(); + this.req[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_closeSession_args.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_closeSession_args'); + if (this.req !== null && this.req !== undefined) { + output.writeFieldBegin('req', Thrift.Type.STRUCT, 1); + this.req[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_closeSession_result = function(args) { +}; +IClientRPCService_closeSession_result.prototype = {}; +IClientRPCService_closeSession_result.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + if (ftype == Thrift.Type.STOP) { + break; + } + input.skip(ftype); + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_closeSession_result.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_closeSession_result'); + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_executeQueryStatement_args = function(args) { + this.req = null; + if (args) { + if (args.req !== undefined && args.req !== null) { + this.req = new ttypes.TSExecuteStatementReq(args.req); + } + } +}; +IClientRPCService_executeQueryStatement_args.prototype = {}; +IClientRPCService_executeQueryStatement_args.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.STRUCT) { + this.req = new ttypes.TSExecuteStatementReq(); + this.req[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_executeQueryStatement_args.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_executeQueryStatement_args'); + if (this.req !== null && this.req !== undefined) { + output.writeFieldBegin('req', Thrift.Type.STRUCT, 1); + this.req[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_executeQueryStatement_result = function(args) { + this.success = null; + if (args) { + if (args.success !== undefined && args.success !== null) { + this.success = new ttypes.TSExecuteStatementResp(args.success); + } + } +}; +IClientRPCService_executeQueryStatement_result.prototype = {}; +IClientRPCService_executeQueryStatement_result.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 0: + if (ftype == Thrift.Type.STRUCT) { + this.success = new ttypes.TSExecuteStatementResp(); + this.success[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_executeQueryStatement_result.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_executeQueryStatement_result'); + if (this.success !== null && this.success !== undefined) { + output.writeFieldBegin('success', Thrift.Type.STRUCT, 0); + this.success[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_requestStatementId_args = function(args) { + this.sessionId = null; + if (args) { + if (args.sessionId !== undefined && args.sessionId !== null) { + this.sessionId = args.sessionId; + } + } +}; +IClientRPCService_requestStatementId_args.prototype = {}; +IClientRPCService_requestStatementId_args.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.I64) { + this.sessionId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_requestStatementId_args.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_requestStatementId_args'); + if (this.sessionId !== null && this.sessionId !== undefined) { + output.writeFieldBegin('sessionId', Thrift.Type.I64, 1); + output.writeI64(this.sessionId); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_requestStatementId_result = function(args) { + this.success = null; + if (args) { + if (args.success !== undefined && args.success !== null) { + this.success = args.success; + } + } +}; +IClientRPCService_requestStatementId_result.prototype = {}; +IClientRPCService_requestStatementId_result.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 0: + if (ftype == Thrift.Type.I64) { + this.success = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_requestStatementId_result.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_requestStatementId_result'); + if (this.success !== null && this.success !== undefined) { + output.writeFieldBegin('success', Thrift.Type.I64, 0); + output.writeI64(this.success); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_insertRecord_args = function(args) { + this.req = null; + if (args) { + if (args.req !== undefined && args.req !== null) { + this.req = new ttypes.TSInsertRecordReq(args.req); + } + } +}; +IClientRPCService_insertRecord_args.prototype = {}; +IClientRPCService_insertRecord_args.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.STRUCT) { + this.req = new ttypes.TSInsertRecordReq(); + this.req[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_insertRecord_args.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_insertRecord_args'); + if (this.req !== null && this.req !== undefined) { + output.writeFieldBegin('req', Thrift.Type.STRUCT, 1); + this.req[Symbol.for("write")](output); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCService_insertRecord_result = function(args) { + this.success = null; + if (args) { + if (args.success !== undefined && args.success !== null) { + this.success = args.success; + } + } +}; +IClientRPCService_insertRecord_result.prototype = {}; +IClientRPCService_insertRecord_result.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 0: + if (ftype == Thrift.Type.I32) { + this.success = input.readI32(); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +IClientRPCService_insertRecord_result.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('IClientRPCService_insertRecord_result'); + if (this.success !== null && this.success !== undefined) { + output.writeFieldBegin('success', Thrift.Type.I32, 0); + output.writeI32(this.success); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var IClientRPCServiceClient = exports.Client = function(output, pClass) { + this.output = output; + this.pClass = pClass; + this._seqid = 0; + this._reqs = {}; +}; +IClientRPCServiceClient.prototype = {}; +IClientRPCServiceClient.prototype.seqid = function() { return this._seqid; }; +IClientRPCServiceClient.prototype.new_seqid = function() { return this._seqid += 1; }; + +IClientRPCServiceClient.prototype.openSession = function(req, callback) { + this._seqid = this.new_seqid(); + if (callback === undefined) { + var _defer = Q.defer(); + this._reqs[this.seqid()] = function(error, result) { + if (error) { + _defer.reject(error); + } else { + _defer.resolve(result); + } + }; + this.send_openSession(req); + return _defer.promise; + } else { + this._reqs[this.seqid()] = callback; + this.send_openSession(req); + } +}; + +IClientRPCServiceClient.prototype.send_openSession = function(req) { + var output = new this.pClass(this.output); + var params = { + req: req + }; + var args = new IClientRPCService_openSession_args(params); + try { + output.writeMessageBegin('openSession', Thrift.MessageType.CALL, this.seqid()); + args[Symbol.for("write")](output); + output.writeMessageEnd(); + return this.output.flush(); + } + catch (e) { + delete this._reqs[this.seqid()]; + if (typeof output.reset === 'function') { + output.reset(); + } + throw e; + } +}; + +IClientRPCServiceClient.prototype.recv_openSession = function(input,mtype,rseqid) { + var callback = this._reqs[rseqid] || function() {}; + delete this._reqs[rseqid]; + if (mtype == Thrift.MessageType.EXCEPTION) { + var x = new Thrift.TApplicationException(); + x[Symbol.for("read")](input); + input.readMessageEnd(); + return callback(x); + } + var result = new IClientRPCService_openSession_result(); + result[Symbol.for("read")](input); + input.readMessageEnd(); + + if (null !== result.success) { + return callback(null, result.success); + } + return callback('openSession failed: unknown result'); +}; + +IClientRPCServiceClient.prototype.closeSession = function(req, callback) { + this._seqid = this.new_seqid(); + if (callback === undefined) { + var _defer = Q.defer(); + this._reqs[this.seqid()] = function(error, result) { + if (error) { + _defer.reject(error); + } else { + _defer.resolve(result); + } + }; + this.send_closeSession(req); + return _defer.promise; + } else { + this._reqs[this.seqid()] = callback; + this.send_closeSession(req); + } +}; + +IClientRPCServiceClient.prototype.send_closeSession = function(req) { + var output = new this.pClass(this.output); + var params = { + req: req + }; + var args = new IClientRPCService_closeSession_args(params); + try { + output.writeMessageBegin('closeSession', Thrift.MessageType.CALL, this.seqid()); + args[Symbol.for("write")](output); + output.writeMessageEnd(); + return this.output.flush(); + } + catch (e) { + delete this._reqs[this.seqid()]; + if (typeof output.reset === 'function') { + output.reset(); + } + throw e; + } +}; + +IClientRPCServiceClient.prototype.recv_closeSession = function(input,mtype,rseqid) { + var callback = this._reqs[rseqid] || function() {}; + delete this._reqs[rseqid]; + if (mtype == Thrift.MessageType.EXCEPTION) { + var x = new Thrift.TApplicationException(); + x[Symbol.for("read")](input); + input.readMessageEnd(); + return callback(x); + } + var result = new IClientRPCService_closeSession_result(); + result[Symbol.for("read")](input); + input.readMessageEnd(); + + callback(null); +}; + +IClientRPCServiceClient.prototype.executeQueryStatement = function(req, callback) { + this._seqid = this.new_seqid(); + if (callback === undefined) { + var _defer = Q.defer(); + this._reqs[this.seqid()] = function(error, result) { + if (error) { + _defer.reject(error); + } else { + _defer.resolve(result); + } + }; + this.send_executeQueryStatement(req); + return _defer.promise; + } else { + this._reqs[this.seqid()] = callback; + this.send_executeQueryStatement(req); + } +}; + +IClientRPCServiceClient.prototype.send_executeQueryStatement = function(req) { + var output = new this.pClass(this.output); + var params = { + req: req + }; + var args = new IClientRPCService_executeQueryStatement_args(params); + try { + output.writeMessageBegin('executeQueryStatement', Thrift.MessageType.CALL, this.seqid()); + args[Symbol.for("write")](output); + output.writeMessageEnd(); + return this.output.flush(); + } + catch (e) { + delete this._reqs[this.seqid()]; + if (typeof output.reset === 'function') { + output.reset(); + } + throw e; + } +}; + +IClientRPCServiceClient.prototype.recv_executeQueryStatement = function(input,mtype,rseqid) { + var callback = this._reqs[rseqid] || function() {}; + delete this._reqs[rseqid]; + if (mtype == Thrift.MessageType.EXCEPTION) { + var x = new Thrift.TApplicationException(); + x[Symbol.for("read")](input); + input.readMessageEnd(); + return callback(x); + } + var result = new IClientRPCService_executeQueryStatement_result(); + result[Symbol.for("read")](input); + input.readMessageEnd(); + + if (null !== result.success) { + return callback(null, result.success); + } + return callback('executeQueryStatement failed: unknown result'); +}; + +IClientRPCServiceClient.prototype.requestStatementId = function(sessionId, callback) { + this._seqid = this.new_seqid(); + if (callback === undefined) { + var _defer = Q.defer(); + this._reqs[this.seqid()] = function(error, result) { + if (error) { + _defer.reject(error); + } else { + _defer.resolve(result); + } + }; + this.send_requestStatementId(sessionId); + return _defer.promise; + } else { + this._reqs[this.seqid()] = callback; + this.send_requestStatementId(sessionId); + } +}; + +IClientRPCServiceClient.prototype.send_requestStatementId = function(sessionId) { + var output = new this.pClass(this.output); + var params = { + sessionId: sessionId + }; + var args = new IClientRPCService_requestStatementId_args(params); + try { + output.writeMessageBegin('requestStatementId', Thrift.MessageType.CALL, this.seqid()); + args[Symbol.for("write")](output); + output.writeMessageEnd(); + return this.output.flush(); + } + catch (e) { + delete this._reqs[this.seqid()]; + if (typeof output.reset === 'function') { + output.reset(); + } + throw e; + } +}; + +IClientRPCServiceClient.prototype.recv_requestStatementId = function(input,mtype,rseqid) { + var callback = this._reqs[rseqid] || function() {}; + delete this._reqs[rseqid]; + if (mtype == Thrift.MessageType.EXCEPTION) { + var x = new Thrift.TApplicationException(); + x[Symbol.for("read")](input); + input.readMessageEnd(); + return callback(x); + } + var result = new IClientRPCService_requestStatementId_result(); + result[Symbol.for("read")](input); + input.readMessageEnd(); + + if (null !== result.success) { + return callback(null, result.success); + } + return callback('requestStatementId failed: unknown result'); +}; + +IClientRPCServiceClient.prototype.insertRecord = function(req, callback) { + this._seqid = this.new_seqid(); + if (callback === undefined) { + var _defer = Q.defer(); + this._reqs[this.seqid()] = function(error, result) { + if (error) { + _defer.reject(error); + } else { + _defer.resolve(result); + } + }; + this.send_insertRecord(req); + return _defer.promise; + } else { + this._reqs[this.seqid()] = callback; + this.send_insertRecord(req); + } +}; + +IClientRPCServiceClient.prototype.send_insertRecord = function(req) { + var output = new this.pClass(this.output); + var params = { + req: req + }; + var args = new IClientRPCService_insertRecord_args(params); + try { + output.writeMessageBegin('insertRecord', Thrift.MessageType.CALL, this.seqid()); + args[Symbol.for("write")](output); + output.writeMessageEnd(); + return this.output.flush(); + } + catch (e) { + delete this._reqs[this.seqid()]; + if (typeof output.reset === 'function') { + output.reset(); + } + throw e; + } +}; + +IClientRPCServiceClient.prototype.recv_insertRecord = function(input,mtype,rseqid) { + var callback = this._reqs[rseqid] || function() {}; + delete this._reqs[rseqid]; + if (mtype == Thrift.MessageType.EXCEPTION) { + var x = new Thrift.TApplicationException(); + x[Symbol.for("read")](input); + input.readMessageEnd(); + return callback(x); + } + var result = new IClientRPCService_insertRecord_result(); + result[Symbol.for("read")](input); + input.readMessageEnd(); + + if (null !== result.success) { + return callback(null, result.success); + } + return callback('insertRecord failed: unknown result'); +}; +var IClientRPCServiceProcessor = exports.Processor = function(handler) { + this._handler = handler; +}; +IClientRPCServiceProcessor.prototype.process = function(input, output) { + var r = input.readMessageBegin(); + if (this['process_' + r.fname]) { + return this['process_' + r.fname].call(this, r.rseqid, input, output); + } else { + input.skip(Thrift.Type.STRUCT); + input.readMessageEnd(); + var x = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN_METHOD, 'Unknown function ' + r.fname); + output.writeMessageBegin(r.fname, Thrift.MessageType.EXCEPTION, r.rseqid); + x[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + } +}; +IClientRPCServiceProcessor.prototype.process_openSession = function(seqid, input, output) { + var args = new IClientRPCService_openSession_args(); + args[Symbol.for("read")](input); + input.readMessageEnd(); + if (this._handler.openSession.length === 1) { + Q.fcall(this._handler.openSession.bind(this._handler), + args.req + ).then(function(result) { + var result_obj = new IClientRPCService_openSession_result({success: result}); + output.writeMessageBegin("openSession", Thrift.MessageType.REPLY, seqid); + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }).catch(function (err) { + var result; + result = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("openSession", Thrift.MessageType.EXCEPTION, seqid); + result[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } else { + this._handler.openSession(args.req, function (err, result) { + var result_obj; + if ((err === null || typeof err === 'undefined')) { + result_obj = new IClientRPCService_openSession_result((err !== null || typeof err === 'undefined') ? err : {success: result}); + output.writeMessageBegin("openSession", Thrift.MessageType.REPLY, seqid); + } else { + result_obj = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("openSession", Thrift.MessageType.EXCEPTION, seqid); + } + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } +}; +IClientRPCServiceProcessor.prototype.process_closeSession = function(seqid, input, output) { + var args = new IClientRPCService_closeSession_args(); + args[Symbol.for("read")](input); + input.readMessageEnd(); + if (this._handler.closeSession.length === 1) { + Q.fcall(this._handler.closeSession.bind(this._handler), + args.req + ).then(function(result) { + var result_obj = new IClientRPCService_closeSession_result({success: result}); + output.writeMessageBegin("closeSession", Thrift.MessageType.REPLY, seqid); + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }).catch(function (err) { + var result; + result = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("closeSession", Thrift.MessageType.EXCEPTION, seqid); + result[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } else { + this._handler.closeSession(args.req, function (err, result) { + var result_obj; + if ((err === null || typeof err === 'undefined')) { + result_obj = new IClientRPCService_closeSession_result((err !== null || typeof err === 'undefined') ? err : {success: result}); + output.writeMessageBegin("closeSession", Thrift.MessageType.REPLY, seqid); + } else { + result_obj = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("closeSession", Thrift.MessageType.EXCEPTION, seqid); + } + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } +}; +IClientRPCServiceProcessor.prototype.process_executeQueryStatement = function(seqid, input, output) { + var args = new IClientRPCService_executeQueryStatement_args(); + args[Symbol.for("read")](input); + input.readMessageEnd(); + if (this._handler.executeQueryStatement.length === 1) { + Q.fcall(this._handler.executeQueryStatement.bind(this._handler), + args.req + ).then(function(result) { + var result_obj = new IClientRPCService_executeQueryStatement_result({success: result}); + output.writeMessageBegin("executeQueryStatement", Thrift.MessageType.REPLY, seqid); + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }).catch(function (err) { + var result; + result = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("executeQueryStatement", Thrift.MessageType.EXCEPTION, seqid); + result[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } else { + this._handler.executeQueryStatement(args.req, function (err, result) { + var result_obj; + if ((err === null || typeof err === 'undefined')) { + result_obj = new IClientRPCService_executeQueryStatement_result((err !== null || typeof err === 'undefined') ? err : {success: result}); + output.writeMessageBegin("executeQueryStatement", Thrift.MessageType.REPLY, seqid); + } else { + result_obj = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("executeQueryStatement", Thrift.MessageType.EXCEPTION, seqid); + } + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } +}; +IClientRPCServiceProcessor.prototype.process_requestStatementId = function(seqid, input, output) { + var args = new IClientRPCService_requestStatementId_args(); + args[Symbol.for("read")](input); + input.readMessageEnd(); + if (this._handler.requestStatementId.length === 1) { + Q.fcall(this._handler.requestStatementId.bind(this._handler), + args.sessionId + ).then(function(result) { + var result_obj = new IClientRPCService_requestStatementId_result({success: result}); + output.writeMessageBegin("requestStatementId", Thrift.MessageType.REPLY, seqid); + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }).catch(function (err) { + var result; + result = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("requestStatementId", Thrift.MessageType.EXCEPTION, seqid); + result[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } else { + this._handler.requestStatementId(args.sessionId, function (err, result) { + var result_obj; + if ((err === null || typeof err === 'undefined')) { + result_obj = new IClientRPCService_requestStatementId_result((err !== null || typeof err === 'undefined') ? err : {success: result}); + output.writeMessageBegin("requestStatementId", Thrift.MessageType.REPLY, seqid); + } else { + result_obj = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("requestStatementId", Thrift.MessageType.EXCEPTION, seqid); + } + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } +}; +IClientRPCServiceProcessor.prototype.process_insertRecord = function(seqid, input, output) { + var args = new IClientRPCService_insertRecord_args(); + args[Symbol.for("read")](input); + input.readMessageEnd(); + if (this._handler.insertRecord.length === 1) { + Q.fcall(this._handler.insertRecord.bind(this._handler), + args.req + ).then(function(result) { + var result_obj = new IClientRPCService_insertRecord_result({success: result}); + output.writeMessageBegin("insertRecord", Thrift.MessageType.REPLY, seqid); + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }).catch(function (err) { + var result; + result = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("insertRecord", Thrift.MessageType.EXCEPTION, seqid); + result[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } else { + this._handler.insertRecord(args.req, function (err, result) { + var result_obj; + if ((err === null || typeof err === 'undefined')) { + result_obj = new IClientRPCService_insertRecord_result((err !== null || typeof err === 'undefined') ? err : {success: result}); + output.writeMessageBegin("insertRecord", Thrift.MessageType.REPLY, seqid); + } else { + result_obj = new Thrift.TApplicationException(Thrift.TApplicationExceptionType.UNKNOWN, err.message); + output.writeMessageBegin("insertRecord", Thrift.MessageType.EXCEPTION, seqid); + } + result_obj[Symbol.for("write")](output); + output.writeMessageEnd(); + output.flush(); + }); + } +}; diff --git a/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.thrift b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.thrift new file mode 100644 index 0000000..aa087f5 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService.thrift @@ -0,0 +1,65 @@ +namespace js IClientRPCService +namespace java IClientRPCService + +enum TSProtocolVersion { + IOTDB_SERVICE_PROTOCOL_V1 = 1, + IOTDB_SERVICE_PROTOCOL_V2 = 2, + IOTDB_SERVICE_PROTOCOL_V3 = 3 +} + +struct TSOpenSessionReq { + 1: optional string username + 2: optional string password + 3: optional i32 client_protocol + 4: optional string zoneId + 5: optional map configuration +} + +struct TSCloseSessionReq { + 1: required i64 sessionId +} + +struct TSExecuteStatementReq { + 1: required i64 sessionId + 2: required string statement + 3: required i64 statementId + 4: required i32 fetchSize + 5: optional i32 timeout +} + +struct TSInsertRecordReq { + 1: required i64 sessionId + 2: required string prefixPath + 3: required list measurements + 4: required binary values + 5: required i64 timestamp + 6: optional bool isAligned +} + +struct TSOpenSessionResp { + 1: required i64 sessionId + 2: required i32 serverProtocolVersion +} + +struct TSExecuteStatementResp { + 1: optional list columns + 2: optional list dataTypeList + 3: optional map columnNameIndexMap + 4: optional i64 queryId + 5: optional QueryDataSet queryDataSet + 6: optional bool ignoreTimeStamp +} + +struct QueryDataSet { + 1: required list valueList + 2: required list bitmapList + 3: required binary time +} + +service IClientRPCService { + TSOpenSessionResp openSession(1: TSOpenSessionReq req) + void closeSession(1: TSCloseSessionReq req) + TSExecuteStatementResp executeQueryStatement(1: TSExecuteStatementReq req) + i64 requestStatementId(1: i64 sessionId) + i32 insertRecord(1: TSInsertRecordReq req) +} diff --git a/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.d.ts b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.d.ts new file mode 100644 index 0000000..4cb465b --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.d.ts @@ -0,0 +1,110 @@ +interface Int64 { + constructor(o?: number | string): this; + toString(): string; + toJson(): string; +} + +export enum TSProtocolVersion { + IOTDB_SERVICE_PROTOCOL_V1 = 1, + IOTDB_SERVICE_PROTOCOL_V2 = 2, + IOTDB_SERVICE_PROTOCOL_V3 = 3, +} + +export class TSOpenSessionReq { + username?: string; + password?: string; + client_protocol?: number; + zoneId?: string; + configuration?: Map; + + constructor(arg?: { + username?: string; + password?: string; + client_protocol?: number; + zoneId?: string; + configuration?: Map; + }) +} + +export class TSCloseSessionReq { + sessionId: Int64; + + constructor(arg?: { + sessionId: Int64; + }) +} + +export class TSExecuteStatementReq { + sessionId: Int64; + statement: string; + statementId: Int64; + fetchSize: number; + timeout?: number; + + constructor(arg?: { + sessionId: Int64; + statement: string; + statementId: Int64; + fetchSize: number; + timeout?: number; + }) +} + +export class TSInsertRecordReq { + sessionId: Int64; + prefixPath: string; + measurements: string[]; + values: Buffer; + timestamp: Int64; + isAligned?: boolean; + + constructor(arg?: { + sessionId: Int64; + prefixPath: string; + measurements: string[]; + values: Buffer; + timestamp: Int64; + isAligned?: boolean; + }) +} + +export class TSOpenSessionResp { + sessionId: Int64; + serverProtocolVersion: number; + + constructor(arg?: { + sessionId: Int64; + serverProtocolVersion: number; + }) +} + +export class TSExecuteStatementResp { + columns?: string[]; + dataTypeList?: number[]; + columnNameIndexMap?: Map; + queryId?: Int64; + queryDataSet?: QueryDataSet; + ignoreTimeStamp?: boolean; + + constructor(arg?: { + columns?: string[]; + dataTypeList?: number[]; + columnNameIndexMap?: Map; + queryId?: Int64; + queryDataSet?: QueryDataSet; + ignoreTimeStamp?: boolean; + }) +} + +export class QueryDataSet { + valueList: Buffer[]; + bitmapList: Buffer[]; + time: Buffer; + + constructor(arg?: { + valueList: Buffer[]; + bitmapList: Buffer[]; + time: Buffer; + }) +} + diff --git a/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.js b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.js new file mode 100644 index 0000000..1e491d3 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/gen-nodejs/IClientRPCService_types.js @@ -0,0 +1,829 @@ +// +// Autogenerated by Thrift Compiler (0.22.0) +// +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +// +"use strict"; + +var thrift = require('thrift'); +var Thrift = thrift.Thrift; +var Q = thrift.Q; +var Int64 = require('node-int64'); + + +var ttypes = module.exports = {}; +ttypes.TSProtocolVersion = { + 'IOTDB_SERVICE_PROTOCOL_V1' : 1, + 'IOTDB_SERVICE_PROTOCOL_V2' : 2, + 'IOTDB_SERVICE_PROTOCOL_V3' : 3 +}; +var TSOpenSessionReq = module.exports.TSOpenSessionReq = function(args) { + this.username = null; + this.password = null; + this.client_protocol = null; + this.zoneId = null; + this.configuration = null; + if (args) { + if (args.username !== undefined && args.username !== null) { + this.username = args.username; + } + if (args.password !== undefined && args.password !== null) { + this.password = args.password; + } + if (args.client_protocol !== undefined && args.client_protocol !== null) { + this.client_protocol = args.client_protocol; + } + if (args.zoneId !== undefined && args.zoneId !== null) { + this.zoneId = args.zoneId; + } + if (args.configuration !== undefined && args.configuration !== null) { + this.configuration = Thrift.copyMap(args.configuration, [null]); + } + } +}; +TSOpenSessionReq.prototype = {}; +TSOpenSessionReq.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.STRING) { + this.username = input.readString(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.STRING) { + this.password = input.readString(); + } else { + input.skip(ftype); + } + break; + case 3: + if (ftype == Thrift.Type.I32) { + this.client_protocol = input.readI32(); + } else { + input.skip(ftype); + } + break; + case 4: + if (ftype == Thrift.Type.STRING) { + this.zoneId = input.readString(); + } else { + input.skip(ftype); + } + break; + case 5: + if (ftype == Thrift.Type.MAP) { + this.configuration = {}; + var _rtmp31 = input.readMapBegin(); + var _size0 = _rtmp31.size || 0; + for (var _i2 = 0; _i2 < _size0; ++_i2) { + var key3 = null; + var val4 = null; + key3 = input.readString(); + val4 = input.readString(); + this.configuration[key3] = val4; + } + input.readMapEnd(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSOpenSessionReq.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSOpenSessionReq'); + if (this.username !== null && this.username !== undefined) { + output.writeFieldBegin('username', Thrift.Type.STRING, 1); + output.writeString(this.username); + output.writeFieldEnd(); + } + if (this.password !== null && this.password !== undefined) { + output.writeFieldBegin('password', Thrift.Type.STRING, 2); + output.writeString(this.password); + output.writeFieldEnd(); + } + if (this.client_protocol !== null && this.client_protocol !== undefined) { + output.writeFieldBegin('client_protocol', Thrift.Type.I32, 3); + output.writeI32(this.client_protocol); + output.writeFieldEnd(); + } + if (this.zoneId !== null && this.zoneId !== undefined) { + output.writeFieldBegin('zoneId', Thrift.Type.STRING, 4); + output.writeString(this.zoneId); + output.writeFieldEnd(); + } + if (this.configuration !== null && this.configuration !== undefined) { + output.writeFieldBegin('configuration', Thrift.Type.MAP, 5); + output.writeMapBegin(Thrift.Type.STRING, Thrift.Type.STRING, Thrift.objectLength(this.configuration)); + for (var kiter5 in this.configuration) { + if (this.configuration.hasOwnProperty(kiter5)) { + var viter6 = this.configuration[kiter5]; + output.writeString(kiter5); + output.writeString(viter6); + } + } + output.writeMapEnd(); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var TSCloseSessionReq = module.exports.TSCloseSessionReq = function(args) { + this.sessionId = null; + if (args) { + if (args.sessionId !== undefined && args.sessionId !== null) { + this.sessionId = args.sessionId; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field sessionId is unset!'); + } + } +}; +TSCloseSessionReq.prototype = {}; +TSCloseSessionReq.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.I64) { + this.sessionId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 0: + input.skip(ftype); + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSCloseSessionReq.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSCloseSessionReq'); + if (this.sessionId !== null && this.sessionId !== undefined) { + output.writeFieldBegin('sessionId', Thrift.Type.I64, 1); + output.writeI64(this.sessionId); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var TSExecuteStatementReq = module.exports.TSExecuteStatementReq = function(args) { + this.sessionId = null; + this.statement = null; + this.statementId = null; + this.fetchSize = null; + this.timeout = null; + if (args) { + if (args.sessionId !== undefined && args.sessionId !== null) { + this.sessionId = args.sessionId; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field sessionId is unset!'); + } + if (args.statement !== undefined && args.statement !== null) { + this.statement = args.statement; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field statement is unset!'); + } + if (args.statementId !== undefined && args.statementId !== null) { + this.statementId = args.statementId; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field statementId is unset!'); + } + if (args.fetchSize !== undefined && args.fetchSize !== null) { + this.fetchSize = args.fetchSize; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field fetchSize is unset!'); + } + if (args.timeout !== undefined && args.timeout !== null) { + this.timeout = args.timeout; + } + } +}; +TSExecuteStatementReq.prototype = {}; +TSExecuteStatementReq.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.I64) { + this.sessionId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.STRING) { + this.statement = input.readString(); + } else { + input.skip(ftype); + } + break; + case 3: + if (ftype == Thrift.Type.I64) { + this.statementId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 4: + if (ftype == Thrift.Type.I32) { + this.fetchSize = input.readI32(); + } else { + input.skip(ftype); + } + break; + case 5: + if (ftype == Thrift.Type.I32) { + this.timeout = input.readI32(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSExecuteStatementReq.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSExecuteStatementReq'); + if (this.sessionId !== null && this.sessionId !== undefined) { + output.writeFieldBegin('sessionId', Thrift.Type.I64, 1); + output.writeI64(this.sessionId); + output.writeFieldEnd(); + } + if (this.statement !== null && this.statement !== undefined) { + output.writeFieldBegin('statement', Thrift.Type.STRING, 2); + output.writeString(this.statement); + output.writeFieldEnd(); + } + if (this.statementId !== null && this.statementId !== undefined) { + output.writeFieldBegin('statementId', Thrift.Type.I64, 3); + output.writeI64(this.statementId); + output.writeFieldEnd(); + } + if (this.fetchSize !== null && this.fetchSize !== undefined) { + output.writeFieldBegin('fetchSize', Thrift.Type.I32, 4); + output.writeI32(this.fetchSize); + output.writeFieldEnd(); + } + if (this.timeout !== null && this.timeout !== undefined) { + output.writeFieldBegin('timeout', Thrift.Type.I32, 5); + output.writeI32(this.timeout); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var TSInsertRecordReq = module.exports.TSInsertRecordReq = function(args) { + this.sessionId = null; + this.prefixPath = null; + this.measurements = null; + this.values = null; + this.timestamp = null; + this.isAligned = null; + if (args) { + if (args.sessionId !== undefined && args.sessionId !== null) { + this.sessionId = args.sessionId; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field sessionId is unset!'); + } + if (args.prefixPath !== undefined && args.prefixPath !== null) { + this.prefixPath = args.prefixPath; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field prefixPath is unset!'); + } + if (args.measurements !== undefined && args.measurements !== null) { + this.measurements = Thrift.copyList(args.measurements, [null]); + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field measurements is unset!'); + } + if (args.values !== undefined && args.values !== null) { + this.values = args.values; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field values is unset!'); + } + if (args.timestamp !== undefined && args.timestamp !== null) { + this.timestamp = args.timestamp; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field timestamp is unset!'); + } + if (args.isAligned !== undefined && args.isAligned !== null) { + this.isAligned = args.isAligned; + } + } +}; +TSInsertRecordReq.prototype = {}; +TSInsertRecordReq.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.I64) { + this.sessionId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.STRING) { + this.prefixPath = input.readString(); + } else { + input.skip(ftype); + } + break; + case 3: + if (ftype == Thrift.Type.LIST) { + this.measurements = []; + var _rtmp38 = input.readListBegin(); + var _size7 = _rtmp38.size || 0; + for (var _i9 = 0; _i9 < _size7; ++_i9) { + var elem10 = null; + elem10 = input.readString(); + this.measurements.push(elem10); + } + input.readListEnd(); + } else { + input.skip(ftype); + } + break; + case 4: + if (ftype == Thrift.Type.STRING) { + this.values = input.readBinary(); + } else { + input.skip(ftype); + } + break; + case 5: + if (ftype == Thrift.Type.I64) { + this.timestamp = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 6: + if (ftype == Thrift.Type.BOOL) { + this.isAligned = input.readBool(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSInsertRecordReq.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSInsertRecordReq'); + if (this.sessionId !== null && this.sessionId !== undefined) { + output.writeFieldBegin('sessionId', Thrift.Type.I64, 1); + output.writeI64(this.sessionId); + output.writeFieldEnd(); + } + if (this.prefixPath !== null && this.prefixPath !== undefined) { + output.writeFieldBegin('prefixPath', Thrift.Type.STRING, 2); + output.writeString(this.prefixPath); + output.writeFieldEnd(); + } + if (this.measurements !== null && this.measurements !== undefined) { + output.writeFieldBegin('measurements', Thrift.Type.LIST, 3); + output.writeListBegin(Thrift.Type.STRING, this.measurements.length); + for (var iter11 in this.measurements) { + if (this.measurements.hasOwnProperty(iter11)) { + iter11 = this.measurements[iter11]; + output.writeString(iter11); + } + } + output.writeListEnd(); + output.writeFieldEnd(); + } + if (this.values !== null && this.values !== undefined) { + output.writeFieldBegin('values', Thrift.Type.STRING, 4); + output.writeBinary(this.values); + output.writeFieldEnd(); + } + if (this.timestamp !== null && this.timestamp !== undefined) { + output.writeFieldBegin('timestamp', Thrift.Type.I64, 5); + output.writeI64(this.timestamp); + output.writeFieldEnd(); + } + if (this.isAligned !== null && this.isAligned !== undefined) { + output.writeFieldBegin('isAligned', Thrift.Type.BOOL, 6); + output.writeBool(this.isAligned); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var TSOpenSessionResp = module.exports.TSOpenSessionResp = function(args) { + this.sessionId = null; + this.serverProtocolVersion = null; + if (args) { + if (args.sessionId !== undefined && args.sessionId !== null) { + this.sessionId = args.sessionId; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field sessionId is unset!'); + } + if (args.serverProtocolVersion !== undefined && args.serverProtocolVersion !== null) { + this.serverProtocolVersion = args.serverProtocolVersion; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field serverProtocolVersion is unset!'); + } + } +}; +TSOpenSessionResp.prototype = {}; +TSOpenSessionResp.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.I64) { + this.sessionId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.I32) { + this.serverProtocolVersion = input.readI32(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSOpenSessionResp.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSOpenSessionResp'); + if (this.sessionId !== null && this.sessionId !== undefined) { + output.writeFieldBegin('sessionId', Thrift.Type.I64, 1); + output.writeI64(this.sessionId); + output.writeFieldEnd(); + } + if (this.serverProtocolVersion !== null && this.serverProtocolVersion !== undefined) { + output.writeFieldBegin('serverProtocolVersion', Thrift.Type.I32, 2); + output.writeI32(this.serverProtocolVersion); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var TSExecuteStatementResp = module.exports.TSExecuteStatementResp = function(args) { + this.columns = null; + this.dataTypeList = null; + this.columnNameIndexMap = null; + this.queryId = null; + this.queryDataSet = null; + this.ignoreTimeStamp = null; + if (args) { + if (args.columns !== undefined && args.columns !== null) { + this.columns = Thrift.copyList(args.columns, [null]); + } + if (args.dataTypeList !== undefined && args.dataTypeList !== null) { + this.dataTypeList = Thrift.copyList(args.dataTypeList, [null]); + } + if (args.columnNameIndexMap !== undefined && args.columnNameIndexMap !== null) { + this.columnNameIndexMap = Thrift.copyMap(args.columnNameIndexMap, [null]); + } + if (args.queryId !== undefined && args.queryId !== null) { + this.queryId = args.queryId; + } + if (args.queryDataSet !== undefined && args.queryDataSet !== null) { + this.queryDataSet = new ttypes.QueryDataSet(args.queryDataSet); + } + if (args.ignoreTimeStamp !== undefined && args.ignoreTimeStamp !== null) { + this.ignoreTimeStamp = args.ignoreTimeStamp; + } + } +}; +TSExecuteStatementResp.prototype = {}; +TSExecuteStatementResp.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.LIST) { + this.columns = []; + var _rtmp313 = input.readListBegin(); + var _size12 = _rtmp313.size || 0; + for (var _i14 = 0; _i14 < _size12; ++_i14) { + var elem15 = null; + elem15 = input.readString(); + this.columns.push(elem15); + } + input.readListEnd(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.LIST) { + this.dataTypeList = []; + var _rtmp317 = input.readListBegin(); + var _size16 = _rtmp317.size || 0; + for (var _i18 = 0; _i18 < _size16; ++_i18) { + var elem19 = null; + elem19 = input.readI32(); + this.dataTypeList.push(elem19); + } + input.readListEnd(); + } else { + input.skip(ftype); + } + break; + case 3: + if (ftype == Thrift.Type.MAP) { + this.columnNameIndexMap = {}; + var _rtmp321 = input.readMapBegin(); + var _size20 = _rtmp321.size || 0; + for (var _i22 = 0; _i22 < _size20; ++_i22) { + var key23 = null; + var val24 = null; + key23 = input.readString(); + val24 = input.readI32(); + this.columnNameIndexMap[key23] = val24; + } + input.readMapEnd(); + } else { + input.skip(ftype); + } + break; + case 4: + if (ftype == Thrift.Type.I64) { + this.queryId = input.readI64(); + } else { + input.skip(ftype); + } + break; + case 5: + if (ftype == Thrift.Type.STRUCT) { + this.queryDataSet = new ttypes.QueryDataSet(); + this.queryDataSet[Symbol.for("read")](input); + } else { + input.skip(ftype); + } + break; + case 6: + if (ftype == Thrift.Type.BOOL) { + this.ignoreTimeStamp = input.readBool(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +TSExecuteStatementResp.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('TSExecuteStatementResp'); + if (this.columns !== null && this.columns !== undefined) { + output.writeFieldBegin('columns', Thrift.Type.LIST, 1); + output.writeListBegin(Thrift.Type.STRING, this.columns.length); + for (var iter25 in this.columns) { + if (this.columns.hasOwnProperty(iter25)) { + iter25 = this.columns[iter25]; + output.writeString(iter25); + } + } + output.writeListEnd(); + output.writeFieldEnd(); + } + if (this.dataTypeList !== null && this.dataTypeList !== undefined) { + output.writeFieldBegin('dataTypeList', Thrift.Type.LIST, 2); + output.writeListBegin(Thrift.Type.I32, this.dataTypeList.length); + for (var iter26 in this.dataTypeList) { + if (this.dataTypeList.hasOwnProperty(iter26)) { + iter26 = this.dataTypeList[iter26]; + output.writeI32(iter26); + } + } + output.writeListEnd(); + output.writeFieldEnd(); + } + if (this.columnNameIndexMap !== null && this.columnNameIndexMap !== undefined) { + output.writeFieldBegin('columnNameIndexMap', Thrift.Type.MAP, 3); + output.writeMapBegin(Thrift.Type.STRING, Thrift.Type.I32, Thrift.objectLength(this.columnNameIndexMap)); + for (var kiter27 in this.columnNameIndexMap) { + if (this.columnNameIndexMap.hasOwnProperty(kiter27)) { + var viter28 = this.columnNameIndexMap[kiter27]; + output.writeString(kiter27); + output.writeI32(viter28); + } + } + output.writeMapEnd(); + output.writeFieldEnd(); + } + if (this.queryId !== null && this.queryId !== undefined) { + output.writeFieldBegin('queryId', Thrift.Type.I64, 4); + output.writeI64(this.queryId); + output.writeFieldEnd(); + } + if (this.queryDataSet !== null && this.queryDataSet !== undefined) { + output.writeFieldBegin('queryDataSet', Thrift.Type.STRUCT, 5); + this.queryDataSet[Symbol.for("write")](output); + output.writeFieldEnd(); + } + if (this.ignoreTimeStamp !== null && this.ignoreTimeStamp !== undefined) { + output.writeFieldBegin('ignoreTimeStamp', Thrift.Type.BOOL, 6); + output.writeBool(this.ignoreTimeStamp); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + +var QueryDataSet = module.exports.QueryDataSet = function(args) { + this.valueList = null; + this.bitmapList = null; + this.time = null; + if (args) { + if (args.valueList !== undefined && args.valueList !== null) { + this.valueList = Thrift.copyList(args.valueList, [null]); + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field valueList is unset!'); + } + if (args.bitmapList !== undefined && args.bitmapList !== null) { + this.bitmapList = Thrift.copyList(args.bitmapList, [null]); + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field bitmapList is unset!'); + } + if (args.time !== undefined && args.time !== null) { + this.time = args.time; + } else { + throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field time is unset!'); + } + } +}; +QueryDataSet.prototype = {}; +QueryDataSet.prototype[Symbol.for("read")] = function(input) { + input.readStructBegin(); + while (true) { + var ret = input.readFieldBegin(); + var ftype = ret.ftype; + var fid = ret.fid; + if (ftype == Thrift.Type.STOP) { + break; + } + switch (fid) { + case 1: + if (ftype == Thrift.Type.LIST) { + this.valueList = []; + var _rtmp330 = input.readListBegin(); + var _size29 = _rtmp330.size || 0; + for (var _i31 = 0; _i31 < _size29; ++_i31) { + var elem32 = null; + elem32 = input.readBinary(); + this.valueList.push(elem32); + } + input.readListEnd(); + } else { + input.skip(ftype); + } + break; + case 2: + if (ftype == Thrift.Type.LIST) { + this.bitmapList = []; + var _rtmp334 = input.readListBegin(); + var _size33 = _rtmp334.size || 0; + for (var _i35 = 0; _i35 < _size33; ++_i35) { + var elem36 = null; + elem36 = input.readBinary(); + this.bitmapList.push(elem36); + } + input.readListEnd(); + } else { + input.skip(ftype); + } + break; + case 3: + if (ftype == Thrift.Type.STRING) { + this.time = input.readBinary(); + } else { + input.skip(ftype); + } + break; + default: + input.skip(ftype); + } + input.readFieldEnd(); + } + input.readStructEnd(); + return; +}; + +QueryDataSet.prototype[Symbol.for("write")] = function(output) { + output.writeStructBegin('QueryDataSet'); + if (this.valueList !== null && this.valueList !== undefined) { + output.writeFieldBegin('valueList', Thrift.Type.LIST, 1); + output.writeListBegin(Thrift.Type.STRING, this.valueList.length); + for (var iter37 in this.valueList) { + if (this.valueList.hasOwnProperty(iter37)) { + iter37 = this.valueList[iter37]; + output.writeBinary(iter37); + } + } + output.writeListEnd(); + output.writeFieldEnd(); + } + if (this.bitmapList !== null && this.bitmapList !== undefined) { + output.writeFieldBegin('bitmapList', Thrift.Type.LIST, 2); + output.writeListBegin(Thrift.Type.STRING, this.bitmapList.length); + for (var iter38 in this.bitmapList) { + if (this.bitmapList.hasOwnProperty(iter38)) { + iter38 = this.bitmapList[iter38]; + output.writeBinary(iter38); + } + } + output.writeListEnd(); + output.writeFieldEnd(); + } + if (this.time !== null && this.time !== undefined) { + output.writeFieldBegin('time', Thrift.Type.STRING, 3); + output.writeBinary(this.time); + output.writeFieldEnd(); + } + output.writeFieldStop(); + output.writeStructEnd(); + return; +}; + diff --git a/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts b/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts new file mode 100644 index 0000000..3ede2fb --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts @@ -0,0 +1,553 @@ +import { SupportedMessageDataTypes } from "./../utils/iotdb-constants"; +import { Client as ThriftClient } from "../gen-nodejs/IClientRPCService"; +import { + TSExecuteStatementReq, + TSOpenSessionReq, + TSOpenSessionResp, + TSProtocolVersion, + TSCloseSessionReq, + TSInsertRecordReq, + Int64, +} from "../gen-nodejs/IClientRPCService_types"; +import thrift from "thrift"; +import { IotDBInt64 } from "../utils/Int64"; +import { HandlerBase } from "../../HandlerBase"; +import { SessionDataSet } from "../utils/SessionDataSet"; +import { IoTDBDataInterpreter } from "../utils/IoTDBDataInterpreter"; +import { + createDataPointsSchema, + isSupportedDataPoint, + SupportedDataPoints, +} from "../config/iotdb-config"; +import { databaseParams, databaseConfig } from "../config/database-params"; +import { + logMessage, + logError, + logErrorStr, + logWithColor, + MessageType, + COLORS, +} from "../../../../utils/logger"; +import { createErrorMessage } from "../../../../utils/error-message-helper"; +import { WebSocket, Message, STATUS_ERRORS } from "../../../utils/data_types"; + +export class IoTDBHandler extends HandlerBase { + private client: ThriftClient | null; + private sendMessageToClients: + | ((ws: WebSocket, message: Message) => void) + | null; + private sessionId: Int64 | undefined; + private readonly protocolVersion: number; + private statementId: Int64 | undefined; + private readonly fetchSize: number; + private dataPointsSchema: SupportedDataPoints; + + constructor() { + super(); + if (!databaseConfig) { + throw new Error("Invalid database configuration."); + } + + this.client = null; + this.sendMessageToClients = null; + this.sessionId = undefined; + this.protocolVersion = TSProtocolVersion.IOTDB_SERVICE_PROTOCOL_V3; + this.statementId = undefined; + this.fetchSize = databaseConfig.fetchSize; + this.dataPointsSchema = {}; + } + + async authenticateAndConnect( + sendMessageToClients: (ws: WebSocket, message: Message) => void + ): Promise { + try { + this.sendMessageToClients = sendMessageToClients; + + const connection = thrift.createConnection( + databaseConfig!.iotdbHost, + databaseConfig!.iotdbPort!, + { + transport: thrift.TFramedTransport, + protocol: thrift.TBinaryProtocol, + } + ); + + logMessage( + `Connect to IoTDB, host: ${databaseConfig!.iotdbHost} port: ${databaseConfig!.iotdbPort}` + ); + + this.client = thrift.createClient(ThriftClient, connection); + + connection.on("error", (error: Error) => { + logError("thrift connection error", error); + }); + + const supportedDataPoint: SupportedDataPoints = + this.getSupportedDataPoints() as SupportedDataPoints; + this.dataPointsSchema = createDataPointsSchema(supportedDataPoint); + + logMessage("Successfully connected to IoTDB using thrift"); + } catch (error: unknown) { + logError("Failed to authenticate with IoTDB", error); + } + } + + protected async read(message: Message, ws: WebSocket): Promise { + if (this.areNodesValid(message, ws)) { + try { + await this.openSessionIfNeeded(); + const responseNodes = await this.queryLastFields(message, ws); + if (responseNodes.length > 0) { + const responseMessage = this.createUpdateMessage( + message, + responseNodes + ); + this.sendMessageToClient(ws, responseMessage); + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "read", + STATUS_ERRORS.NOT_FOUND, + `No data found with the Id: ${message.id}` + ) + ); + } + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + this.sendMessageToClient( + ws, + createErrorMessage("read", STATUS_ERRORS.NOT_FOUND, errMsg) + ); + } finally { + await this.closeSessionIfNeeded(); + } + } + } + + protected async write(message: Message, ws: WebSocket): Promise { + if (this.areNodesValid(message, ws)) { + try { + await this.openSessionIfNeeded(); + const data = this.createObjectToInsert(message); + let measurements: string[] = []; + let dataTypes: string[] = []; + let values: any[] = []; + + for (const [key, value] of Object.entries(data)) { + measurements.push(key); + dataTypes.push(this.dataPointsSchema[key]); + values.push(value); + } + + const tree = message.tree as keyof typeof databaseParams; + if (!tree || !databaseParams[tree]) { + throw new Error(`Invalid tree specified: ${message.tree}`); + } + + const deviceId = databaseParams[tree].databaseName; + const status = await this.insertRecord( + deviceId, + measurements, + dataTypes, + values + ); + + logWithColor( + `Record inserted to device ${deviceId}, status code: `.concat( + JSON.stringify(status) + ), + COLORS.GREY + ); + + const responseNodes = await this.queryLastFields(message, ws); + + if (responseNodes.length) { + const responseMessage = this.createUpdateMessage( + message, + responseNodes + ); + this.sendMessageToClient(ws, responseMessage); + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "write", + STATUS_ERRORS.NOT_FOUND, + `No data found with the Id: ${message.id}` + ) + ); + } + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + this.sendMessageToClient( + ws, + createErrorMessage( + "write", + STATUS_ERRORS.SERVICE_UNAVAILABLE, + `Failed writing data. ${errMsg}` + ) + ); + } finally { + await this.closeSessionIfNeeded(); + } + } + } + + /** + * Opens a session with the IoTDB server using the provided credentials and configuration. + */ + private async openSession(): Promise { + if (this.sessionId) { + logMessage("The session is already opened."); + return; + } + + const openSessionReq = new TSOpenSessionReq({ + username: databaseConfig!.iotdbUser, + password: databaseConfig!.iotdbPassword, + client_protocol: this.protocolVersion, + zoneId: databaseConfig!.timeZoneId, + configuration: new Map([["version", "V_0_13"]]), + }); + + try { + if (!this.client) { + throw new Error("Client is not initialized"); + } + const resp: TSOpenSessionResp = + await this.client.openSession(openSessionReq); + + if (this.protocolVersion !== resp.serverProtocolVersion) { + logMessage( + "Protocol differ, Client version is " + + this.protocolVersion + + ", but Server version is " + + resp.serverProtocolVersion + ); + // version is less than 0.10 + if (resp.serverProtocolVersion === 0) { + throw new Error("Protocol not supported."); + } + } + + this.sessionId = resp.sessionId; + if (this.sessionId) { + this.statementId = await this.client?.requestStatementId( + this.sessionId + ); + } else { + throw new Error( + "Session ID is undefined, cannot request statement ID." + ); + } + logMessage("Session started!"); + } catch (error: unknown) { + logError("Failed starting session with IoTDB", error); + } + } + + /** + * Closes the current session if it is not already closed. + */ + private async closeSession(): Promise { + if (!this.sessionId) { + logMessage("Session is already closed."); + return; + } + + const req = new TSCloseSessionReq({ sessionId: this.sessionId! }); + + try { + await this.client?.closeSession(req); + } catch (error: unknown) { + logError( + "Error occurs when closing session at server. Maybe server is down. Error message", + error + ); + } finally { + this.sessionId = undefined; + logMessage("Session closed!"); + } + } + + private async openSessionIfNeeded(): Promise { + if (!this.sessionId) { + await this.openSession(); + } + } + + private async closeSessionIfNeeded(): Promise { + if (this.sessionId) { + this.closeSession(); + } + } + + /** + * Validates the nodes in a message against the schema of a media element. + * + * @param message - The message object containing details for the request. + * @param ws - The WebSocket object for communication. + * @returns - Returns true if all nodes are valid against the schema, otherwise false. + */ + private areNodesValid(message: Message, ws: WebSocket): boolean { + const { type } = message; + + const errorData = this.validateNodesAgainstSchema( + message, + this.dataPointsSchema + ); + + if (errorData) { + logErrorStr( + `Error validating message nodes against schema: ${JSON.stringify(errorData)}` + ); + this.sendMessageToClient( + ws, + createErrorMessage( + `${type}`, + STATUS_ERRORS.NOT_FOUND, + JSON.stringify(errorData) + ) + ); + + return false; + } + return true; + } + + /** + * Queries the last fields from the database based on the provided message and sends the response to the client. + * + * @param message - The message object containing the query details. + * @param ws - The WebSocket connection to send the response to. + */ + private async queryLastFields( + message: Message, + ws: WebSocket + ): Promise> { + const { id: objectId, tree } = message; + + if (!tree || !(tree in databaseParams)) { + const errorMsg = `Invalid or undefined tree provided: ${tree}`; + logErrorStr(errorMsg); + this.sendMessageToClient( + ws, + createErrorMessage("read", STATUS_ERRORS.NOT_FOUND, errorMsg) + ); + return []; + } + const { databaseName, dataPointId } = + databaseParams[tree as keyof typeof databaseParams]; + const fieldsToSearch = this.extractDataPointsFromNodes(message).join(", "); + const sql = `SELECT ${fieldsToSearch} FROM ${databaseName} WHERE ${dataPointId} = '${objectId}' ORDER BY Time ASC`; + + try { + const sessionDataSet = await this.executeQueryStatement(sql); + + // Check if sessionDataSet is not an instance of SessionDataSet, and handle the error + if (!(sessionDataSet instanceof SessionDataSet)) { + throw new Error( + "Failed to retrieve session data. Invalid session dataset." + ); + } + + const mediaElements: any[] = []; + while (sessionDataSet.hasNext()) { + mediaElements.push(sessionDataSet.next()); + } + + const latestValues: Record = {}; + mediaElements.forEach((mediaElement) => { + const transformedMediaElement = Object.fromEntries( + Object.entries(mediaElement).map(([key, value]) => { + const newKey = this.transformDataPointsWithDots(key); + return [newKey, value]; + }) + ); + + const transformedObject = + IoTDBDataInterpreter.extractNodesFromTimeseries( + transformedMediaElement, + databaseName + ); + + Object.entries(transformedObject).forEach(([key, value]) => { + if (value !== null && !isNaN(value)) { + latestValues[key] = value; + } + }); + }); + + return Object.entries(latestValues).map(([name, value]) => ({ + name, + value, + })); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + this.sendMessageToClient( + ws, + createErrorMessage("read", STATUS_ERRORS.SERVICE_UNAVAILABLE, errMsg) + ); + return []; + } + } + + /** + * Executes a SQL query statement asynchronously. + * + * @param sql - The SQL query statement to be executed. + * @returns - Returns a SessionDataSet object if the query is successful, + * otherwise returns an empty object. + * @throws - Throws an error if the session is not open. + */ + private async executeQueryStatement(sql: string): Promise { + try { + if (!this.sessionId) { + throw new Error("Session is not open. Please authenticate first."); + } + if (!this.statementId) { + throw new Error("Missing statement ID"); + } + + const request = new TSExecuteStatementReq({ + sessionId: this.sessionId, + statement: sql, + statementId: this.statementId, + fetchSize: this.fetchSize, + timeout: 0, + }); + + const resp = await this.client?.executeQueryStatement(request); + if (!resp || !resp.queryDataSet || !resp.queryDataSet.valueList) { + throw new Error("Failed to execute query or retrieve valid data."); + } + return new SessionDataSet( + resp.columns ?? [], + resp.dataTypeList ?? [], + resp.columnNameIndexMap + ? Object.fromEntries(resp.columnNameIndexMap) + : {}, + resp.queryId ?? new IotDBInt64(-1), + this.client!, + this.statementId!, + this.sessionId!, + resp.queryDataSet, + resp.ignoreTimeStamp ?? false + ); + } catch (error: unknown) { + logError("Failed executing query statement", error); + throw error; + } + } + + /** + * Inserts a record into the time series database. + * @param deviceId - The ID of the device. + * @param timestamp - The timestamp of the record. + * @param measurements - Array of measurement names. + * @param dataTypes - Array of data types for each value. + * @param values - Array of values to be inserted. + * @param isAligned - Flag indicating if the data is aligned. + * @returns - A promise that resolves with the result of the insertion. + * @throws - Throws an error if lengths of data types, values, and measurements do not match. + */ + private async insertRecord( + deviceId: string, + measurements: string[], + dataTypes: string[], + values: any[], + isAligned = false + ): Promise { + if (!this.sessionId) { + throw new Error("Session is not open. Please authenticate first."); + } + if ( + values.length !== dataTypes.length || + values.length !== measurements.length + ) { + throw "Length of data types does not equal to length of values!"; + } + + // Validate the dataTypes before using them + const validatedDataTypes: (keyof typeof SupportedMessageDataTypes)[] = []; + + dataTypes.forEach((dataType) => { + if (isSupportedDataPoint(dataType)) { + validatedDataTypes.push(dataType); // Add valid data types + } else { + throw new Error(`Unsupported data type: ${dataType}`); + } + }); + + const valuesInBytes = IoTDBDataInterpreter.serializeValues( + validatedDataTypes, + values + ); + + const request = new TSInsertRecordReq({ + sessionId: this.sessionId!, + prefixPath: deviceId, + measurements: measurements, + values: valuesInBytes, + timestamp: new IotDBInt64(Date.now()), + isAligned: isAligned, + }); + + return await this.client?.insertRecord(request); + } + + /** + * Extracts data point names from the given message. + * + * This function checks if the message has a single node or multiple nodes and + * extracts the names accordingly. + * + * @param message - The message containing node(s). + * @returns An array of data point names. + */ + private extractDataPointsFromNodes(message: Message): string[] { + let dataPoints: string[] = []; + + if (message.node) { + dataPoints.push( + this.transformDataPointsWithUnderscores(message.node.name) + ); + } else if (message.nodes) { + dataPoints = message.nodes.map((node) => + this.transformDataPointsWithUnderscores(node.name) + ); + } + return dataPoints; + } + + /** + * Creates an object to be inserted into the database based on the provided message. + * The object is constructed using the message's ID and its associated nodes. + * + * @param message - The message object containing the ID, tree, and nodes. + * @returns An object representing the data to be inserted. + * @throws Will throw an error if the tree is invalid or undefined. + */ + private createObjectToInsert(message: Message): Record { + const { id, tree } = message; + + if (!tree || !(tree in databaseParams)) { + throw new Error(`Invalid or undefined tree provided: ${tree}`); + } + const { dataPointId } = databaseParams[tree as keyof typeof databaseParams]; + const data: Record = { [dataPointId]: id }; + + if (message.node) { + data[this.transformDataPointsWithUnderscores(message.node.name)] = + message.node.value; + } else if (message.nodes) { + message.nodes.forEach((node) => { + data[this.transformDataPointsWithUnderscores(node.name)] = node.value; + }); + } + return data; + } +} + +export default IoTDBHandler; diff --git a/cdsp/information-layer/handlers/src/iotdb/utils/Int64.ts b/cdsp/information-layer/handlers/src/iotdb/utils/Int64.ts new file mode 100644 index 0000000..9f8b6a6 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/utils/Int64.ts @@ -0,0 +1,44 @@ +// Implementing Int64 class that matches the structure expected by Thrift +import { Int64 } from "../gen-nodejs/IClientRPCService_types"; + +/** + * Represents a 64-bit integer for IoT applications. + * Implements the Int64 interface. + */ +export class IotDBInt64 implements Int64 { + private value: bigint; // Holds the 64-bit integer value + + /** + * Constructs an instance of IotDBInt64. + * @param input - An optional number or string to initialize the 64-bit integer. + * If no input is provided, it defaults to 0. + */ + constructor(input?: number | string) { + this.value = BigInt(input ?? 0); // Handle input as either number or string + } + + /** + * A placeholder constructor method that is not implemented. + * @param o - An optional number or string parameter. + * @throws {Error} Throws an error indicating that the method is not implemented. + */ + ["constructor"](o?: number | string): this { + throw new Error("Method not implemented."); + } + + /** + * Converts the 64-bit integer to its string representation. + * @returns {string} The string representation of the 64-bit integer. + */ + toString(): string { + return this.value.toString(); + } + + /** + * Converts the 64-bit integer to its JSON representation. + * @returns {string} The JSON representation of the 64-bit integer. + */ + toJson(): string { + return this.toString(); // You can customize this if needed + } +} diff --git a/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBDataInterpreter.ts b/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBDataInterpreter.ts new file mode 100644 index 0000000..d8a7e74 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBDataInterpreter.ts @@ -0,0 +1,121 @@ +import { IoTDBDataType, SupportedMessageDataTypes } from "./iotdb-constants"; + +export class IoTDBDataInterpreter { + /** + * Serializes values based on the specified data types. + * @param dataTypes - Array of data types to serialize the values as. + * @param values - Array of values to be serialized. + * @returns Serialized values as a Buffer. + */ + static serializeValues( + dataTypes: (keyof typeof SupportedMessageDataTypes)[], + values: any[], + ): Buffer { + function serializeBoolean(value: boolean): (number | boolean)[] { + return [IoTDBDataType.BOOLEAN, value]; + } + + function serializeInt32(value: number): (number | Uint8Array)[] { + const int32 = new Int32Array([value]); + const uint8 = new Uint8Array(int32.buffer).reverse(); + return [IoTDBDataType.INT32, ...uint8]; + } + + function serializeInt64(value: bigint): (number | Uint8Array)[] { + const bigint64 = new BigInt64Array([value]); + const uint8 = new Uint8Array(bigint64.buffer).reverse(); + return [IoTDBDataType.INT64, ...uint8]; + } + + function serializeFloat(value: number): (number | Uint8Array)[] { + const float32 = new Float32Array([value]); + const uint8 = new Uint8Array(float32.buffer).reverse(); + return [IoTDBDataType.FLOAT, ...uint8]; + } + + function serializeDouble(value: number): (number | Uint8Array)[] { + const float64 = new Float64Array([value]); + const uint8 = new Uint8Array(float64.buffer).reverse(); + return [IoTDBDataType.DOUBLE, ...uint8]; + } + + function serializeText(value: string): (number | Uint8Array)[] { + const utf8arr = Buffer.from(value); + const int32 = new Uint32Array([utf8arr.length]); + const uint8 = new Uint8Array(int32.buffer).reverse(); + return [IoTDBDataType.TEXT, ...uint8, ...utf8arr]; + } + + const serializedValues: (number | Uint8Array | boolean)[] = []; + + for (let i = 0; i < dataTypes.length; i++) { + switch (dataTypes[i]) { + case SupportedMessageDataTypes.boolean: + serializedValues.push(...serializeBoolean(values[i])); + break; + case SupportedMessageDataTypes.int8: + case SupportedMessageDataTypes.int16: + case SupportedMessageDataTypes.uint8: + case SupportedMessageDataTypes.uint16: + serializedValues.push(...serializeInt32(values[i])); + break; + // case SupportedMessageDataTypes.int64: // this type is not supported by now, see: cdsp/information-layer/handlers/iotdb/utils/iotdb-constants.js + // serializedValues.push(...serializeInt64(values[i])); + // break; + case SupportedMessageDataTypes.float: + serializedValues.push(...serializeFloat(values[i])); + break; + case SupportedMessageDataTypes.double: + serializedValues.push(...serializeDouble(values[i])); + break; + case SupportedMessageDataTypes.string: + serializedValues.push(...serializeText(values[i])); + break; + default: + throw new Error(`Unsupported data type: ${dataTypes[i]}`); + } + } + // Convert to Uint8Array and pass it to Buffer.from + const flattenedValues = new Uint8Array( + serializedValues.flat().map((val) => Number(val)), + ); + return Buffer.from(flattenedValues); + } + + /** + * Extracts and transforms timeseries nodes from the given object with the required message format. + * + * @param obj - The object containing timeseries data. + * @param databaseName - The database name to match and remove from the keys. + * @returns A new object with transformed keys and their corresponding values. + */ + static extractNodesFromTimeseries( + obj: Record, + databaseName: string, + ): Record { + return Object.entries(obj).reduce( + (acc: Record, [key, value]) => { + if (key.startsWith(databaseName)) { + const newKey = key.replace(`${databaseName}.`, ""); + acc[newKey] = value; + } + return acc; + }, + {}, + ); + } + + /** + * This function converts a buffer to a BigInt64Array and extracts the timestamp. + * + * @param buffer - The buffer containing the timestamp. + * @returns An object containing the extracted timestamp. + */ + static extractTimestamp(buffer: Buffer): { timestamp: bigint } { + const reverseBuffer = Buffer.from(buffer.subarray(0, 8).reverse()); + const uinit8Buffer = new Uint8Array(reverseBuffer).buffer; + const timestamp = new BigInt64Array(uinit8Buffer)[0]; + + return { timestamp }; + } +} diff --git a/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBRpcDataSet.ts b/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBRpcDataSet.ts new file mode 100644 index 0000000..de8a98a --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/utils/IoTDBRpcDataSet.ts @@ -0,0 +1,223 @@ +import { IoTDBDataType } from "./iotdb-constants"; +import { Int64 } from "../gen-nodejs/IClientRPCService_types"; + +export class IoTDBRpcDataSet { + // Static properties + static TIMESTAMP_STR = "Time"; + static START_INDEX = 2; + static FLAG = 0x80; + + // Private fields + private sql: string | null = null; + private columnNameList: string[]; + private columnTypeList: IoTDBDataType[]; + private queryId: Int64; + private client: any; + private statementId: Int64; + private sessionId: Int64; + private queryDataSet: any; + private ignoreTimestamp: boolean; + private fetchSize: number; + private columnOrdinalDict: Map; + private columnTypeDeduplicatedList: (IoTDBDataType | null)[]; + private timeBytes: Buffer; + private currentBitmap: Buffer[]; + private hasCachedRecord: boolean; + private value: (Buffer | null)[]; + private emptyResultSet: boolean; + private rowsIndex: number; + private columnSize: number; + + constructor( + columnNameList: string[], + columnTypeList: IoTDBDataType[], + columnNameIndex: { [key: string]: number } | null, + queryId: Int64, + client: any, + statementId: Int64, + sessionId: Int64, + queryDataSet: any, + ignoreTimestamp: boolean, + fetchSize: number + ) { + this.columnNameList = []; + this.columnTypeList = []; + this.queryId = queryId; + this.client = client; + this.statementId = statementId; + this.sessionId = sessionId; + this.queryDataSet = queryDataSet; + this.ignoreTimestamp = ignoreTimestamp; + this.fetchSize = fetchSize; + this.columnSize = columnNameList.length; + this.columnOrdinalDict = new Map(); + this.columnTypeDeduplicatedList = []; + this.hasCachedRecord = false; + this.emptyResultSet = false; + this.rowsIndex = 0; + + if (!ignoreTimestamp) { + this.columnNameList.push(IoTDBRpcDataSet.TIMESTAMP_STR); + this.columnTypeList.push(IoTDBDataType.INT64); + this.columnOrdinalDict.set(IoTDBRpcDataSet.TIMESTAMP_STR, 1); + } + + if (columnNameIndex !== null) { + for (let j = 0; j < columnNameIndex.length; j++) { + this.columnTypeDeduplicatedList.push(null); + } + + for (let i = 0; i < columnNameList.length; i++) { + const name = columnNameList[i]; + this.columnNameList.push(name); + this.columnTypeList.push(columnTypeList[i]); + if (!this.columnOrdinalDict.has(name)) { + const index = columnNameIndex[name]; + this.columnOrdinalDict.set(name, index + IoTDBRpcDataSet.START_INDEX); + this.columnTypeDeduplicatedList[index] = columnTypeList[i]; + } + } + } + + this.timeBytes = Buffer.alloc(0); + this.currentBitmap = Array(this.columnTypeDeduplicatedList.length).fill( + Buffer.alloc(0) + ); + this.value = Array(this.columnTypeDeduplicatedList.length).fill(null); + } + + next(): boolean { + if (this.hasCachedResult()) { + this.constructOneRow(); + return true; + } + if (this.emptyResultSet) { + return true; + } + this.fetchResults(); + if (this.hasCachedResult()) { + this.constructOneRow(); + return true; + } + return false; + } + + getHasCachedRecord(): boolean { + return this.hasCachedRecord; + } + + setHasCachedRecord(value: boolean): void { + this.hasCachedRecord = value; + } + + getTimeBytes(): Buffer { + return this.timeBytes; + } + + getColumnSize(): number { + return this.columnSize; + } + + getIgnoreTimestamp(): boolean { + return this.ignoreTimestamp; + } + + getColumnNames(): string[] { + return this.columnNameList; + } + + getColumnOrdinalDict(): Map { + return this.columnOrdinalDict; + } + + isNullByIndex(columnIndex: number): boolean { + const index = + this.columnOrdinalDict.get(this.findColumnNameByIndex(columnIndex))! - + IoTDBRpcDataSet.START_INDEX; + if (index < 0) { + return true; + } + return this.isNull(index, this.rowsIndex - 1); + } + + getValues(): (Buffer | null)[] { + return this.value; + } + + getColumnTypeDeduplicatedList(): (IoTDBDataType | null)[] { + return this.columnTypeDeduplicatedList; + } + + findColumnNameByIndex(columnIndex: number): string { + if (columnIndex <= 0 || columnIndex > this.columnNameList.length) { + throw new Error("Column index out of range"); + } + return this.columnNameList[columnIndex - 1]; + } + + private hasCachedResult(): boolean { + return this.queryDataSet !== null && this.queryDataSet.time.length !== 0; + } + + private constructOneRow(): void { + this.timeBytes = this.queryDataSet.time.slice(0, 8); + this.queryDataSet.time = this.queryDataSet.time.slice(8); + + for (let i = 0; i < this.queryDataSet.bitmapList.length; i++) { + const bitmapBuffer = this.queryDataSet.bitmapList[i]; + + if (this.rowsIndex % 8 === 0) { + this.currentBitmap[i] = bitmapBuffer[0]; + this.queryDataSet.bitmapList[i] = bitmapBuffer.slice(1); + } + + if (!this.isNull(i, this.rowsIndex)) { + const valueBuffer = this.queryDataSet.valueList[i]; + const dataType = this.columnTypeDeduplicatedList[i]; + + switch (dataType) { + case IoTDBDataType.BOOLEAN: + this.value[i] = valueBuffer.slice(0, 1); + this.queryDataSet.valueList[i] = valueBuffer.slice(1); + break; + case IoTDBDataType.INT32: + this.value[i] = valueBuffer.slice(0, 4); + this.queryDataSet.valueList[i] = valueBuffer.slice(4); + break; + case IoTDBDataType.INT64: + this.value[i] = valueBuffer.slice(0, 8); + this.queryDataSet.valueList[i] = valueBuffer.slice(8); + break; + case IoTDBDataType.FLOAT: + this.value[i] = valueBuffer.slice(0, 4); + this.queryDataSet.valueList[i] = valueBuffer.slice(4); + break; + case IoTDBDataType.DOUBLE: + this.value[i] = valueBuffer.slice(0, 8); + this.queryDataSet.valueList[i] = valueBuffer.slice(8); + break; + case IoTDBDataType.TEXT: + const length = valueBuffer.readInt32BE(0); + this.value[i] = valueBuffer.slice(4, 4 + length); + this.queryDataSet.valueList[i] = valueBuffer.slice(4 + length); + break; + default: + throw new Error(`Unsupported data type: ${dataType}`); + } + } + } + this.rowsIndex += 1; + this.hasCachedRecord = true; + } + + private isNull(index: number, rowNum: number): boolean { + const bitmap = this.currentBitmap[index]; + const shift = rowNum % 8; + const bitmapValue = bitmap.readUInt8(0); // Read the first byte as an unsigned integer + return ((IoTDBRpcDataSet.FLAG >> shift) & (bitmapValue & 0xff)) === 0; + } + + private fetchResults(): void { + this.rowsIndex = 0; + } +} diff --git a/cdsp/information-layer/handlers/src/iotdb/utils/SessionDataSet.ts b/cdsp/information-layer/handlers/src/iotdb/utils/SessionDataSet.ts new file mode 100644 index 0000000..364fdc2 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/utils/SessionDataSet.ts @@ -0,0 +1,111 @@ +import { IoTDBDataType } from "./iotdb-constants"; +import { IoTDBRpcDataSet } from "./IoTDBRpcDataSet"; +import { IoTDBDataInterpreter } from "./IoTDBDataInterpreter"; +import { Int64 } from "../gen-nodejs/IClientRPCService_types"; + +// Define the dataTypeProcessors map with specific function types +const dataTypeProcessors: { + [key in IoTDBDataType]: ( + bytes: Uint8Array | Buffer, + ) => number | bigint | string; +} = { + [IoTDBDataType.BOOLEAN]: (bytes) => + new Int8Array(new Uint8Array(bytes.slice(0, 1).reverse()).buffer)[0], + [IoTDBDataType.INT32]: (bytes) => + new Int32Array(new Uint8Array(bytes.slice(0, 4).reverse()).buffer)[0], + [IoTDBDataType.INT64]: (bytes) => + new BigInt64Array(new Uint8Array(bytes.slice(0, 8).reverse()).buffer)[0], + [IoTDBDataType.FLOAT]: (bytes) => + new Float32Array(new Uint8Array(bytes.slice(0, 4).reverse()).buffer)[0], + [IoTDBDataType.DOUBLE]: (bytes) => + new Float64Array(new Uint8Array(bytes.slice(0, 8).reverse()).buffer)[0], + [IoTDBDataType.TEXT]: (bytes) => bytes.toString(), +}; + +export class SessionDataSet { + private iotdbRpcDataSet: IoTDBRpcDataSet; + + constructor( + columnNameList: string[], + columnTypeList: IoTDBDataType[], + columnNameIndex: { [key: string]: number }, + queryId: Int64, + client: any, + statementId: Int64, + sessionId: Int64, + queryDataSet: any, + ignoreTimestamp: boolean, + ) { + this.iotdbRpcDataSet = new IoTDBRpcDataSet( + columnNameList, + columnTypeList, + columnNameIndex, + queryId, + client, + statementId, + sessionId, + queryDataSet, + ignoreTimestamp, + 1024, // Buffer size or default value + ); + } + + hasNext(): boolean { + return this.iotdbRpcDataSet.next(); + } + + next(): { [key: string]: any } | null { + if (!this.iotdbRpcDataSet.getHasCachedRecord()) { + if (!this.hasNext()) { + return null; + } + } + this.iotdbRpcDataSet.setHasCachedRecord(false); + return this.constructRowRecordFromValueArray(); + } + + private constructRowRecordFromValueArray(): { [key: string]: any } { + const time64 = IoTDBDataInterpreter.extractTimestamp( + this.iotdbRpcDataSet.getTimeBytes(), + ); + const obj: { [key: string]: any } = { timestamp: time64 }; + + for (let i = 0; i < this.iotdbRpcDataSet.getColumnSize(); ++i) { + let index = i + 1; + let dataSetColumnIndex = i + IoTDBRpcDataSet.START_INDEX; + + if (this.iotdbRpcDataSet.getIgnoreTimestamp()) { + index -= 1; + dataSetColumnIndex -= 1; + } + + const columnName = this.iotdbRpcDataSet.getColumnNames()[index]; + const location = + this.iotdbRpcDataSet.getColumnOrdinalDict().get(columnName)! - + IoTDBRpcDataSet.START_INDEX; + + if (!this.iotdbRpcDataSet.isNullByIndex(dataSetColumnIndex)) { + const valueBytes = this.iotdbRpcDataSet.getValues()[location]; + const dataType = + this.iotdbRpcDataSet.getColumnTypeDeduplicatedList()[location]; + const tsName = + this.iotdbRpcDataSet.findColumnNameByIndex(dataSetColumnIndex); + + if (dataType !== null && valueBytes !== null) { + if (dataTypeProcessors[dataType]) { + obj[tsName] = dataTypeProcessors[dataType](valueBytes); + } else { + throw new Error(`Unsupported data type: ${dataType}`); + } + } else { + obj[tsName] = null; + } + } else { + const tsName = + this.iotdbRpcDataSet.findColumnNameByIndex(dataSetColumnIndex); + obj[tsName] = null; + } + } + return obj; + } +} diff --git a/cdsp/information-layer/handlers/src/iotdb/utils/iotdb-constants.ts b/cdsp/information-layer/handlers/src/iotdb/utils/iotdb-constants.ts new file mode 100644 index 0000000..f2cebd2 --- /dev/null +++ b/cdsp/information-layer/handlers/src/iotdb/utils/iotdb-constants.ts @@ -0,0 +1,21 @@ +// Define IoTDBDataType as an enum for better type safety +export enum IoTDBDataType { + BOOLEAN = 0, + INT32 = 1, + INT64 = 2, + FLOAT = 3, + DOUBLE = 4, + TEXT = 5, +} + +// Define SupportedMessageDataTypes as a frozen object with a string-to-string mapping +export const SupportedMessageDataTypes = { + boolean: "boolean", + string: "string", + float: "float", + double: "double", + int8: "int8", + int16: "int16", + uint8: "uint8", + uint16: "uint16", +} as const; diff --git a/cdsp/information-layer/handlers/src/realmdb/README.md b/cdsp/information-layer/handlers/src/realmdb/README.md new file mode 100644 index 0000000..8eefddb --- /dev/null +++ b/cdsp/information-layer/handlers/src/realmdb/README.md @@ -0,0 +1,53 @@ +# RealmDB + +This directory contains the RealmDB Handler as Node.js application. As [RealmDB](https://www.mongodb.com/docs/atlas/device-sdks/sdk/node/) is an embedded database, the RealmDB Handler directly embeds the RealmSDK which creates the [RealmDB database](https://github.com/realm/realm-js) file(s) automatically in the working directory during runtime of RealmDB Handler. + +# Features + +- **Authentication**: Authenticates with MongoDB Realm using an API key. +- **Read Data**: Retrieves data from the Realm database using a VIN (for VSS object) object ID. +- **Write Data**: Write data to the Realm database using a VIN (for VSS object) as object ID.- +- **Subscribe to Changes**: Listens to changes in specific data objects using VIN (for VSS object) as object ID and sends updates to WebSocket clients. +- **Unsubscribe to Changes**: Unsubscribe listener to a specific data object using VIN (for VSS object) as object ID. +- **Unsubscribe client**: Unsubscribe all listeners applied to a client. +- **Error Handling**: Logs and handles errors during database operations and synchronization. + +# Configure RealmDB + +Before the Database-Router can start the RealmDB Handler without any errors you need to start and run Docker containers defined in a [Docker Compose file](/docker/). + +## Create a ATLAS Cloud instance + +To get APIKey and AppID you need to set up a [ATLAS cloud](https://cloud.mongodb.com/) instance and App Services. There is a free Tier solution (Status as of May 29, 2024) and you will find a lot of documentation in the internet how to set up everything. + +## Configure of a RealmDB Handler + +Create (if it does not exist) `/docker/.env` and add the following environment variables, replacing the app id and the api key with yours. + +```sh + ######################### + # GENERAL CONFIGURATION # + ######################### + + # HANDLER_TYPE define the database to initialize + HANDLER_TYPE=realmdb + # DATA_POINTS_SCHEMA_FILE is the YAML or JSON file containing all data points supported. See the ../../config/README.md for more information. + DATA_POINTS_SCHEMA_FILE=vss_data_points.yaml + ######################### + # REALMDB CONFIGURATION # + ######################### + + # VERSION must be 0 or a positive integer. This is used for versioning the RealmDB configuration schema. + VERSION_REALMDB_SCHEMA=0 + + # Access to ATLAS Cloud instance + REALMDB_APP_ID="your-app-key" + REALMDB_API_KEY="your-api-key" +``` + +> [!WARNING] +> Do not commit this file to GitHub! + +## Starting the RealmDB handler + +You do not need to start RealmDB Handler manually. It is started by the DB-Router like described [here](../../../README.md). diff --git a/cdsp/information-layer/handlers/src/realmdb/config/database-params.ts b/cdsp/information-layer/handlers/src/realmdb/config/database-params.ts new file mode 100644 index 0000000..f1c5e64 --- /dev/null +++ b/cdsp/information-layer/handlers/src/realmdb/config/database-params.ts @@ -0,0 +1,51 @@ +import { getEnvValue } from "../../../config/config"; +import { DatabaseParamsRecord } from "../../../utils/data_types"; + +interface RealmDBConfig { + storePath: string; + realmAppId: string; + realmApiKey: string; +} + +/** + * Contains the definition of the database name and its identifier data point for each catalog. + */ +export const mediaElementsParams: Readonly = { + VSS: { + databaseName: "Vehicles", // name of the configured RealmDB for the VSS database + dataPointId: "Vehicle_VehicleIdentification_VIN", // data point used as element ID + }, +}; + +/** + * Retrieves the database configuration as a read-only object. + * + * This function fetches the Realm application ID and API key from the environment variables. + * It ensures that both values are present, throwing an error if either is missing. + * + * @returns The database configuration object containing the store path, Realm app ID, and API key. + * + * @throws If the REALMDB_APP_ID or REALMDB_API_KEY environment variables are not set. + */ +const getDatabaseConfig = (): Readonly => { + const realmAppId = getEnvValue("REALMDB_APP_ID"); + const realmApiKey = getEnvValue("REALMDB_API_KEY"); + + if (!realmAppId) { + throw new Error("REALMDB_APP_ID is required, but no default has been set"); + } else if (!realmApiKey) { + throw new Error("REALMDB_API_KEY is required, but no default has been set"); + } + + return { + storePath: "myrealm12.realm", + realmAppId, + realmApiKey, + }; +}; + +/** + * Exports the database configuration depending on the handler type. + */ +export const databaseConfig = + getEnvValue("HANDLER_TYPE") === "realmdb" ? getDatabaseConfig() : undefined; diff --git a/cdsp/information-layer/handlers/src/realmdb/config/realm-config.ts b/cdsp/information-layer/handlers/src/realmdb/config/realm-config.ts new file mode 100644 index 0000000..f61616b --- /dev/null +++ b/cdsp/information-layer/handlers/src/realmdb/config/realm-config.ts @@ -0,0 +1,113 @@ +import { mediaElementsParams, databaseConfig } from "./database-params"; +import { getEnvValue } from "../../../config/config"; +import { User, Configuration, SyncConfiguration } from "realm"; // Import Realm SDK types +import { logError } from "../../../../utils/logger"; + +// Define the type for supported data points +export interface SupportedDataPoints { + [key: string]: + | "boolean" + | "string" + | "float" + | "double" + | "int8" + | "int16" + | "uint8" + | "uint16"; +} + +// Define the Realm schema structure +interface RealmSchema { + primaryKey: string; + name: string; + properties: Record; +} + +/** + * Creates a Realm schema for media elements based on the supported endpoints. + * + * @param supportedEndpoints - An object representing the data points + * that are supported, where keys are the property names and values are their data types. + * @returns The constructed Realm schema object containing the primary key, + * name of the schema, and properties with their corresponding data types. + * @throws Throws an error if an unsupported data type is encountered in the + * provided supportedEndpoints. + */ +function createMediaElementSchema( + supportedEndpoints: SupportedDataPoints, +): RealmSchema { + const properties: Record = { _id: "string" }; + + Object.entries(supportedEndpoints).forEach(([key, value]) => { + switch (value) { + case "boolean": + properties[key] = "bool"; + break; + case "string": + case "float": + case "double": + properties[key] = value; + break; + case "int8": + case "int16": + case "uint8": + case "uint16": + properties[key] = "int"; + break; + default: + throw new Error( + `The initialized data points contain an unsupported data type: ${value}`, + ); + } + }); + + return { + primaryKey: "_id", + name: mediaElementsParams.VSS.databaseName, // Assuming mediaElementsParams contains 'VSS' + properties: properties, + }; +} + +// Get the schema version from environment variables +const getSchemaVersion = (): number => { + const schemaVersion = parseInt( + getEnvValue("VERSION_REALMDB_SCHEMA") ?? "", + 10, + ); + if (isNaN(schemaVersion)) { + throw new Error( + "Version must be specified as an ENV variable and it must be 0 or a positive integer", + ); + } + return schemaVersion; +}; + +/** + * Configures the Realm database settings. + * + * @param user - The user object for authentication. + * @param supportedEndpoints - List of supported endpoints for the media element schema. + * @return The configuration object for the Realm database. + */ +export function realmConfig( + user: User, + supportedDataPoints: SupportedDataPoints, +): Configuration { + const mediaElementSchema = createMediaElementSchema(supportedDataPoints); + + return { + schema: [mediaElementSchema], + path: databaseConfig!.storePath, + sync: { + user: user, + flexible: true, + error: (error: Error) => { + logError("Realm sync error:", error); + }, + } as SyncConfiguration, // Cast sync as SyncConfiguration + schemaVersion: getSchemaVersion(), + // migration: (oldRealm: any, newRealm: any) => { + // // Migration logic here + // }, + }; +} diff --git a/cdsp/information-layer/handlers/src/realmdb/src/RealmDbHandler.ts b/cdsp/information-layer/handlers/src/realmdb/src/RealmDbHandler.ts new file mode 100644 index 0000000..37e3c46 --- /dev/null +++ b/cdsp/information-layer/handlers/src/realmdb/src/RealmDbHandler.ts @@ -0,0 +1,463 @@ +import Realm from "realm"; +import { v4 as uuidv4 } from "uuid"; +import { HandlerBase } from "../../../../handlers/src/HandlerBase"; +import { mediaElementsParams, databaseConfig } from "../config/database-params"; +import { realmConfig, SupportedDataPoints } from "../config/realm-config"; +import { + logMessage, + logError, + logWithColor, + MessageType, + COLORS, + logErrorStr, +} from "../../../../utils/logger"; +import { createErrorMessage } from "../../../../utils/error-message-helper"; +import { WebSocket, Message, STATUS_ERRORS } from "../../../utils/data_types"; + +// Define a type for changes +interface Changes { + deleted: boolean; + changedProperties: string[]; +} + +/** + * Parses the response from a media element change event. + * + * @param changes - The object containing the changed properties. + * @param mediaElement - The media element object with updated properties. + * @returns An array of objects, each containing the name and value of a changed property. + */ +function parseOnMediaElementChangeResponse( + changes: Changes, + mediaElement: any +) { + return changes.changedProperties.map((prop) => ({ + name: prop, + value: mediaElement[prop], + })); +} + +export class RealmDBHandler extends HandlerBase { + private realm: Realm | null; + private sendMessageToClients: + | ((ws: WebSocket, message: Message) => void) + | null; + private listeners: Map>; + + constructor() { + super(); + if (!databaseConfig) { + throw new Error("Invalid database configuration."); + } + this.realm = null; + this.sendMessageToClients = null; + this.listeners = new Map(); + } + + async authenticateAndConnect( + sendMessageToClients: (ws: WebSocket, message: Message) => void + ): Promise { + try { + this.sendMessageToClients = sendMessageToClients; + + const app = new Realm.App({ id: databaseConfig!.realmAppId }); + const credentials = Realm.Credentials.apiKey(databaseConfig!.realmApiKey); + const user = await app.logIn(credentials); + logMessage("Successfully authenticated to RealmDB"); + + const supportedDataPoints = + this.getSupportedDataPoints() as SupportedDataPoints; + const realmConfigObj = realmConfig(user, supportedDataPoints); + this.realm = await Realm.open(realmConfigObj); + logMessage("Connection established successfully"); + + for (const [key, value] of Object.entries(mediaElementsParams)) { + try { + const databaseName = value.databaseName; + await this.realm.objects(databaseName).subscribe(); + logMessage(`Subscribed to the database ${key}: ${databaseName}`); + } catch (error: unknown) { + logError("Error subscribing databases", error); + } + } + } catch (error: unknown) { + logError("Failed to authenticate with Realm", error); + } + } + + protected async read(message: Message, ws: WebSocket): Promise { + if (this.areNodesValid(message, ws)) { + try { + const updateMessage = await this.getMessageData(message); + this.sendMessageToClient(ws, updateMessage); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + logError("Error reading object from Realm", error); + this.sendMessageToClient( + ws, + createErrorMessage("read", STATUS_ERRORS.NOT_FOUND, errMsg) + ); + } + } + } + + protected async write(message: Message, ws: WebSocket): Promise { + if (this.areNodesValid(message, ws)) { + try { + const mediaElement = await this.getMediaElement(message); + const nodes = message.node ? [message.node] : message.nodes; + + const transformAndAssign = (element: any, nodes: any[]) => { + nodes.forEach(({ name, value }) => { + const prop = this.transformDataPointsWithUnderscores(name); + element[prop] = value; + }); + }; + + this.realm?.write(() => { + if (mediaElement) { + transformAndAssign(mediaElement, nodes ?? []); + } else { + if (!message.tree || !mediaElementsParams[message.tree]) { + const errorMessage = + "Tree is undefined or does not exist in mediaElementsParams"; + logErrorStr(errorMessage); + this.sendMessageToClient( + ws, + createErrorMessage( + "write", + STATUS_ERRORS.NOT_FOUND, + errorMessage + ) + ); + return; + } + + const dataPointId = mediaElementsParams[message.tree].dataPointId; + const document = { _id: uuidv4(), [dataPointId]: message.id }; + transformAndAssign(document, nodes ?? []); + const databaseName = mediaElementsParams[message.tree].databaseName; + this.realm?.create(databaseName, document); + } + }); + + await this.read(message, ws); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = `Schema is not compatible for that media element: ${errMsg}`; + logErrorStr(errorMessage); + this.sendMessageToClient( + ws, + createErrorMessage("write", STATUS_ERRORS.NOT_FOUND, errorMessage) + ); + } + } + } + + protected async subscribe(message: Message, ws: WebSocket): Promise { + try { + const mediaElement = await this.getMediaElement(message); + + if (mediaElement) { + const objectId = mediaElement._id; + const { id, tree, uuid } = message; + if (!id || !tree || !mediaElementsParams[tree]) { + const errorMessage = + "Tree or id is undefined or does not exist in mediaElementsParams"; + logErrorStr(errorMessage); + this.sendMessageToClient( + ws, + createErrorMessage("write", STATUS_ERRORS.NOT_FOUND, errorMessage) + ); + return; + } + + const { databaseName, dataPointId } = mediaElementsParams[tree]; + + if (!this.listeners.has(ws)) { + this.listeners.set(ws, new Map()); + } + + if (!this.listeners.get(ws)?.has(id)) { + logWithColor( + `Subscribing element for user '${uuid}': Object ID: ${objectId} with ${dataPointId}: '${id}' on ${databaseName}`, + COLORS.GREY + ); + + const listener = (mediaElement: any, changes: Changes) => + this.onMediaElementChange( + mediaElement, + changes, + { id, tree, uuid }, + ws + ); + + mediaElement.addListener(listener); + + this.listeners.get(ws)?.set(id, { + objectId: objectId, + mediaElement: mediaElement, + listener: listener, + }); + + this.sendMessageToClient( + ws, + this.createSubscribeMessage("subscribe", message, "succeed") + ); + + logWithColor( + `Subscription added! Amount Clients: ${this.listeners.size}`, + COLORS.GREY + ); + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "subscribe", + STATUS_ERRORS.BAD_REQUEST, + `Subscription already done to ${dataPointId}: '${id}'` + ) + ); + } + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "subscribe", + STATUS_ERRORS.BAD_REQUEST, + "Object not found" + ) + ); + } + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : "Unknown error"; + this.sendMessageToClient( + ws, + createErrorMessage( + "subscribe", + STATUS_ERRORS.SERVICE_UNAVAILABLE, + `Subscription process could not finish, try again: ${errMsg}` + ) + ); + } + } + + protected async unsubscribe(message: Message, ws: WebSocket): Promise { + const { id, tree, uuid } = message; + if (!id || !tree || !mediaElementsParams[tree]) { + const errorMessage = + "Tree or id is undefined or does not exist in mediaElementsParams"; + logErrorStr(errorMessage); + this.sendMessageToClient( + ws, + createErrorMessage("write", STATUS_ERRORS.NOT_FOUND, errorMessage) + ); + return; + } + + const { databaseName, dataPointId } = mediaElementsParams[tree]; + + if (this.listeners.has(ws)) { + const wsListeners = this.listeners.get(ws); + if (wsListeners?.has(id)) { + const listener = wsListeners.get(id); + logWithColor( + `Unsubscribing element for user '${uuid}': Object ID: ${listener.objectId} with ${dataPointId}: '${id}' on ${databaseName}`, + COLORS.GREY + ); + listener.mediaElement.removeListener(listener.listener); + wsListeners.delete(id); + + if (wsListeners.size === 0) { + this.listeners.delete(ws); + } + + this.sendMessageToClient( + ws, + this.createSubscribeMessage("unsubscribe", message, "succeed") + ); + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "unsubscribe", + STATUS_ERRORS.BAD_REQUEST, + `No subscription found for VIN: ${id}` + ) + ); + } + } else { + this.sendMessageToClient( + ws, + createErrorMessage( + "unsubscribe", + STATUS_ERRORS.BAD_REQUEST, + `No subscription found for this client` + ) + ); + } + logWithColor( + `Subscription removed! Amount Clients: ${this.listeners.size}`, + COLORS.GREY + ); + } + + async unsubscribe_client(ws: WebSocket): Promise { + this.listeners.delete(ws); + logWithColor( + `All client subscriptions removed! Amount Clients: ${this.listeners.size}`, + COLORS.GREY + ); + } + + /** + * Validates the nodes in a message against the schema of a media element. + * + * @param message - The message object containing details for the request. + * @param ws - The WebSocket object for communication. + * @returns - Returns true if all nodes are valid against the schema, otherwise false. + */ + private areNodesValid(message: Message, ws: WebSocket): boolean { + const { type, tree } = message; + if (!tree || !mediaElementsParams[tree]) { + logErrorStr("Tree is undefined or does not exist in mediaElementsParams"); + return false; + } + + const { databaseName } = mediaElementsParams[tree]; + + const mediaElementSchema = this.realm?.schema.find( + (schema) => schema.name === databaseName + ); + + const errorData = this.validateNodesAgainstSchema( + message, + mediaElementSchema?.properties ?? {} + ); + + if (errorData) { + logErrorStr( + `Error validating message nodes against schema: ${JSON.stringify(errorData)}` + ); + this.sendMessageToClient( + ws, + createErrorMessage( + `${type}`, + STATUS_ERRORS.NOT_FOUND, + JSON.stringify(errorData) + ) + ); + return false; + } + return true; + } + + /** + * Asynchronously processes a message to fetch and handle media data. + * + * @param message - The message object containing details for the request. + * @returns Returns a promise that resolves to an updated message object. + * @throws Throws an error if no media element for the message ID is found. + * @private + */ + private async getMessageData(message: Message): Promise { + const mediaElement = await this.getMediaElement(message); + + if (!mediaElement) { + throw new Error(`No data found with the Id: ${message.id}`); + } + + logWithColor( + `Media Element: \n ${JSON.stringify(mediaElement)}`, + COLORS.GREY + ); + + const responseNodes = this.parseReadResponse(message, mediaElement); + return this.createUpdateMessage(message, responseNodes); + } + + /** + * Parses the response from a read event. + * + * @param message - The message object containing node or nodes information. + * @param queryResponseObj - The query response object containing values to be mapped. + * @returns - A data object with keys from the message nodes and values from the query response. + * @private + */ + private parseReadResponse( + message: Message, + queryResponseObj: any + ): { name: string; value: any }[] { + const data: { name: string; value: any }[] = []; + const nodes = message.node ? [message.node] : message.nodes; + nodes?.forEach((node: any) => { + const prop = this.transformDataPointsWithUnderscores(node.name); + data.push({ + name: node.name, + value: queryResponseObj[prop], + }); + }); + return data; + } + + /** + * Asynchronously retrieves a media element from the database based on the provided message. + * + * @param message - The message containing the id and tree information. + * @returns - The media element object from the database. + * @private + */ + private async getMediaElement(message: Message): Promise { + try { + const { id, tree } = message; + if (!tree || !mediaElementsParams[tree]) { + logErrorStr( + "Tree is undefined or does not exist in mediaElementsParams" + ); + return false; + } + + const { databaseName, dataPointId } = mediaElementsParams[tree]; + return await this.realm + ?.objects(databaseName) + .filtered(`${dataPointId} = '${id}'`)[0]; + } catch (error: unknown) { + logError("Error trying to get media element from Realm", error); + } + } + + /** + * Handles changes to a media element and sends update messages to clients. + * @param mediaElement - The media element that has changed. + * @param changes - An object containing information about the changes. + * @param messageHeader - The header information for the message. + * @param ws - The WebSocket object for communication. + */ + private onMediaElementChange( + mediaElement: any, + changes: Changes, + messageHeader: Pick, + ws: WebSocket + ): void { + logMessage( + "Media element changed", + MessageType.RECEIVED, + `Web-Socket Connection Event Received` + ); + if (changes.deleted) { + logWithColor("MediaElement is deleted", COLORS.YELLOW); + } else { + if (changes.changedProperties.length > 0) { + const responseNodes = parseOnMediaElementChangeResponse( + changes, + mediaElement + ); + const updateMessage = this.createUpdateMessage( + messageHeader, + responseNodes + ); + this.sendMessageToClient(ws, updateMessage); + } + } + } +} diff --git a/cdsp/information-layer/handlers/utils/data_types.ts b/cdsp/information-layer/handlers/utils/data_types.ts new file mode 100644 index 0000000..9e2fd84 --- /dev/null +++ b/cdsp/information-layer/handlers/utils/data_types.ts @@ -0,0 +1,52 @@ +export interface MessageBase { + type: "read" | "write" | "subscribe" | "unsubscribe" | "update"; + id: string; + tree: string; + uuid: string; + dateTime?: string; + status?: string; +} + +export interface MessageWithNode extends MessageBase { + node: { name: string; value: any }; + nodes?: never; // Ensures 'nodes' cannot be present if 'node' is used +} + +export interface MessageWithNodes extends MessageBase { + nodes: Array<{ name: string; value: any }>; + node?: never; // Ensures 'node' cannot be present if 'nodes' is used +} + +export type Message = MessageWithNode | MessageWithNodes; + +export type ErrorMessage = { + category: string; + statusCode: number; + message: string; +}; + +export const STATUS_ERRORS = { + BAD_REQUEST: 400, + NOT_FOUND: 404, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +} as const; + +interface databaseParams { + databaseName: string; + dataPointId: string; +} + +export interface DatabaseParamsRecord { + [key: string]: databaseParams; +} + +export interface WebSocket { + send: (data: string) => void; +} + +export interface DataPointSchema { + [key: string]: any; +} diff --git a/cdsp/information-layer/package-lock.json b/cdsp/information-layer/package-lock.json new file mode 100644 index 0000000..0a6cafa --- /dev/null +++ b/cdsp/information-layer/package-lock.json @@ -0,0 +1,6741 @@ +{ + "name": "information-layer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "information-layer", + "version": "1.0.0", + "dependencies": { + "ajv": "^8.17.1", + "dotenv": "^16.4.5", + "js-yaml": "^4.1.0", + "realm": "^12.13.1", + "thrift": "^0.21.0", + "uuid": "^10.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/ajv": "^0.0.5", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.7.0", + "@types/thrift": "^0.10.17", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.12", + "copyfiles": "^2.4.1", + "eslint": "^9.9.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "3.3.3", + "rimraf": "^6.0.1", + "ts-jest": "^29.2.5", + "typescript": "^5.6.2", + "typescript-eslint": "^8.4.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@realm/fetch": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@realm/fetch/-/fetch-0.1.1.tgz", + "integrity": "sha512-hkTprw79RXGv54Je0DrjpQPLaz4QID2dO3FmthAQQWAkqwyrqMzrCGzJzLlmTKWZFsgLrN8KQyNewod27P+nJg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/ajv": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-0.0.5.tgz", + "integrity": "sha512-0UD98SZLwPk6cTMZoLs8W4XSAWgqT9nyoZiBw+uXEPf1u5h+Dn2ztMG4Is9pDA+9D7GKbCTfIkQK/hNgoWVQcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-int64": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@types/node-int64/-/node-int64-0.4.32.tgz", + "integrity": "sha512-xf/JsSlnXQ+mzvc0IpXemcrO4BrCfpgNpMco+GLcXkFk01k/gW9lGJu+Vof0ZSvHK6DsHJDPSbjFPs36QkWXqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/q": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", + "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/thrift": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@types/thrift/-/thrift-0.10.17.tgz", + "integrity": "sha512-bDX6d5a5ZDWC81tgDv224n/3PKNYfIQJTPHzlbk4vBWJrYXF6Tg1ncaVmP/c3JbGN2AK9p7zmHorJC2D6oejGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/node-int64": "*", + "@types/q": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-or-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-1.3.0.tgz", + "integrity": "sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "license": "ISC", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/realm": { + "version": "12.13.1", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.13.1.tgz", + "integrity": "sha512-fAs70ZCBf1P7htVhOTrDMFHD6SzoGXVsALy6DpOPR6t0LXoK635cKxBMECX3bYdCgI7+riSfdoWXLA/7g5yTSQ==", + "deprecated": "This version uses Atlas Device Sync, please install `realm@community` and read https://github.com/realm/realm-js/blob/main/DEPRECATION.md for more information.", + "hasInstallScript": true, + "license": "apache-2.0", + "dependencies": { + "@realm/fetch": "^0.1.1", + "bson": "^4.7.2", + "debug": "^4.3.4", + "node-machine-id": "^1.1.12", + "path-browserify": "^1.0.1", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react-native": ">=0.71.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + } + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thrift": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/thrift/-/thrift-0.21.0.tgz", + "integrity": "sha512-AW8rwHYjeqXisS8B1iwko2gkNMFwSRJ+bO/W0xTGqRspTD3lGHwZx438+pHdOJ3GwpRAnK7TKbjqPOrHAa7ZwQ==", + "license": "Apache-2.0", + "dependencies": { + "browser-or-node": "^1.2.1", + "isomorphic-ws": "^4.0.1", + "node-int64": "^0.4.0", + "q": "^1.5.0", + "ws": "^5.2.3" + }, + "engines": { + "node": ">= 10.18.0" + } + }, + "node_modules/thrift/node_modules/ws": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.4.tgz", + "integrity": "sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz", + "integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "@typescript-eslint/utils": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/cdsp/information-layer/package.json b/cdsp/information-layer/package.json new file mode 100644 index 0000000..1a7e849 --- /dev/null +++ b/cdsp/information-layer/package.json @@ -0,0 +1,42 @@ +{ + "name": "information-layer", + "version": "1.0.0", + "description": "WebSocket server with handlers for IoTDB and RealmDB", + "private": true, + "main": "dist/router/src/websocket-server.js", + "scripts": { + "copy-files": "npx copyfiles -u 3 ./handlers/config/schema-files/**/* ./dist/handlers/config/schema-files/", + "check-format": "prettier --check './**/*.{ts,tsx}'", + "prebuild": "rimraf /dist", + "lint": "eslint", + "lint-and-fix": "eslint --fix", + "build": "tsc && npm run copy-files", + "start": "node ./dist/router/src/websocket-server.js" + }, + "dependencies": { + "ajv": "^8.17.1", + "dotenv": "^16.4.5", + "js-yaml": "^4.1.0", + "realm": "^12.13.1", + "thrift": "^0.21.0", + "uuid": "^10.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/ajv": "^0.0.5", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.7.0", + "@types/thrift": "^0.10.17", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.12", + "copyfiles": "^2.4.1", + "eslint": "^9.9.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "ts-jest": "^29.2.5", + "typescript": "^5.6.2", + "typescript-eslint": "^8.4.0" + } +} diff --git a/cdsp/information-layer/router/.gitignore b/cdsp/information-layer/router/.gitignore new file mode 100644 index 0000000..58c3437 --- /dev/null +++ b/cdsp/information-layer/router/.gitignore @@ -0,0 +1,2 @@ +# MAC +**/.DS_Store \ No newline at end of file diff --git a/cdsp/information-layer/router/README.md b/cdsp/information-layer/router/README.md new file mode 100644 index 0000000..4f5a7f0 --- /dev/null +++ b/cdsp/information-layer/router/README.md @@ -0,0 +1,4 @@ +# Websocket-Server + +This project is a [WebSocket server](src/websocket-server.ts) that dynamically integrates different database handlers (e.g., RealmDB, IoTDB) based on the [configuration](../handlers/README.md). It listens for incoming WebSocket connections, processes messages according to the specified handler, and broadcasts messages to connected clients. + diff --git a/cdsp/information-layer/router/config/config.ts b/cdsp/information-layer/router/config/config.ts new file mode 100644 index 0000000..33768ca --- /dev/null +++ b/cdsp/information-layer/router/config/config.ts @@ -0,0 +1,13 @@ +import dotenv from "dotenv"; +dotenv.config(); + +// Define the type of the handler, which will be a lowercase string +export const getHandlerType = (): string => { + const handlerType = process.env.HANDLER_TYPE; + + if (!handlerType) { + throw new Error("HANDLER_TYPE must be specified as an ENV variable"); + } + + return handlerType.toLowerCase(); // Ensure it's returned as lowercase +}; diff --git a/cdsp/information-layer/router/src/websocket-server.ts b/cdsp/information-layer/router/src/websocket-server.ts new file mode 100644 index 0000000..aa6f93a --- /dev/null +++ b/cdsp/information-layer/router/src/websocket-server.ts @@ -0,0 +1,96 @@ +import WebSocket, { RawData } from "ws"; +import { RealmDBHandler } from "../../handlers/src/realmdb/src/RealmDbHandler"; +import { IoTDBHandler } from "../../handlers/src/iotdb/src/IoTDBHandler"; +import { HandlerBase } from "../../handlers/src/HandlerBase"; +import { getHandlerType } from "../config/config"; +import { validateMessage } from "../utils/message-validator"; +import { + logMessage, + logError, + logWithColor, + MessageType, + COLORS, +} from "../../utils/logger"; +import { Message } from "../../handlers/utils/data_types"; + +// Define the handler as the base class type +let handler: HandlerBase; + +const handlerType: string = getHandlerType(); + +logWithColor(`\n ** Handler: ${handlerType} ** \n`, COLORS.BOLD); + +// Instantiate the correct handler based on the handler type +switch (handlerType) { + case "realmdb": + handler = new RealmDBHandler(); + break; + case "iotdb": + handler = new IoTDBHandler(); + break; + default: + throw new Error("Unsupported handler type"); +} + +// WebSocket server creation +const server = new WebSocket.Server({ port: 8080 }); + +// Define clients array globally to store connected clients +let clients: WebSocket[] = []; + +// Handle new client connections +server.on("connection", (ws: WebSocket) => { + logWithColor("* Client connected *", COLORS.YELLOW); + clients.push(ws); // Add client to the array + + // Handle messages from the client + ws.on("message", (message: WebSocket.RawData) => { + let messageString = rawDataToString(message); + logMessage(JSON.stringify(messageString), MessageType.RECEIVED); + const validatedMessage = validateMessage(messageString); + + if (validatedMessage instanceof Error) { + logError("Invalid message format", validatedMessage); + JSON.parse(validatedMessage.message).forEach((error: any) => { + ws.send(JSON.stringify(error)); + }); + } else { + // Pass the validated message to the handler + handler.handleMessage(validatedMessage, ws); + } + }); + + // Handle client disconnection + ws.on("close", () => { + handler.unsubscribe_client(ws); // Remove all client listeners + clients = clients.filter((client) => client !== ws); // Remove client from array + logMessage("* Client disconnected *"); + }); +}); + +// Function to broadcast messages to all connected clients +const sendMessageToClients = (message: Message) => { + logMessage(JSON.stringify(message), MessageType.SENT); + clients.forEach((client) => { + client.send(JSON.stringify(message)); + }); +}; + +// Initialize handler and authenticate +logMessage(`Starting authentication and connection ...`); +handler.authenticateAndConnect(sendMessageToClients); + +// Log server start +logWithColor(`Web-Socket server started on ws://localhost:8080\n`, COLORS.BOLD); + +function rawDataToString(message: RawData): string { + if (typeof message === "string") { + return message; + } else if (Buffer.isBuffer(message)) { + return message.toString(); + } else if (message instanceof ArrayBuffer) { + return new TextDecoder().decode(message); + } else { + throw new Error("Unsupported message type"); + } +} diff --git a/cdsp/information-layer/router/utils/message-validator.ts b/cdsp/information-layer/router/utils/message-validator.ts new file mode 100644 index 0000000..4b24385 --- /dev/null +++ b/cdsp/information-layer/router/utils/message-validator.ts @@ -0,0 +1,238 @@ +import Ajv, { ErrorObject } from "ajv"; +import { createErrorMessage } from "../../utils/error-message-helper"; +import { + ErrorMessage, + Message, + STATUS_ERRORS, +} from "../../handlers/utils/data_types"; + +// Initialize AJV with specific options +const ajv = new Ajv({ + allErrors: true, + strict: true, + allowUnionTypes: true, +}); + +// Define the headers for validation +const itemsHeader = ["type", "tree", "id", "uuid"]; +const ERROR_CATEGORY = "messageValidation"; + +// Standard error message template +const standardError: ErrorMessage = { + category: ERROR_CATEGORY, + statusCode: STATUS_ERRORS.BAD_REQUEST, + message: "Received an invalid message.", +}; + +// Helper to create the common structure for different message types +const createCommonStructure = (requestType: string) => ({ + type: { type: "string", enum: [requestType] }, + tree: { type: "string", enum: ["VSS"] }, + id: { type: "string" }, + uuid: { type: "string" }, +}); + +// Define the schemas for message validation +const schemas = { + read: { + type: "object", + properties: { + ...createCommonStructure("read"), + node: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + additionalProperties: false, + }, + }, + required: itemsHeader.concat("node"), + additionalProperties: false, + }, + multiNodeRead: { + type: "object", + properties: { + ...createCommonStructure("read"), + nodes: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + additionalProperties: false, + }, + }, + }, + required: itemsHeader.concat("nodes"), + additionalProperties: false, + }, + write: { + type: "object", + properties: { + ...createCommonStructure("write"), + node: { + type: "object", + properties: { + name: { type: "string" }, + value: { type: ["number", "string", "boolean"] }, + }, + required: ["name", "value"], + additionalProperties: false, + }, + }, + required: itemsHeader.concat("node"), + additionalProperties: false, + }, + multiNodeWrite: { + type: "object", + properties: { + ...createCommonStructure("write"), + nodes: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + value: { type: ["number", "string", "boolean"] }, + }, + required: ["name", "value"], + additionalProperties: false, + }, + }, + }, + required: itemsHeader.concat("nodes"), + additionalProperties: false, + }, + subscribe: { + type: "object", + properties: { + ...createCommonStructure("subscribe"), + }, + required: itemsHeader, + additionalProperties: false, + }, + unsubscribe: { + type: "object", + properties: { + ...createCommonStructure("unsubscribe"), + }, + required: itemsHeader, + additionalProperties: false, + }, +}; + +// Function to customize error messages from AJV validation +const customizeErrorMessage = ( + errors: ErrorObject[] | null | undefined, +): ErrorMessage[] => { + if (!errors) return [standardError]; + + return errors.map((err) => { + const { keyword, instancePath, params } = err; + + switch (keyword) { + case "required": + return createErrorMessage( + ERROR_CATEGORY, + STATUS_ERRORS.BAD_REQUEST, + `Missing required field: ${params.missingProperty}.`, + ); + case "type": + return createErrorMessage( + ERROR_CATEGORY, + STATUS_ERRORS.BAD_REQUEST, + `Invalid type for field '${instancePath}': expected ${params.type}.`, + ); + case "enum": + if (err.instancePath === "/tree") { + return createErrorMessage( + ERROR_CATEGORY, + STATUS_ERRORS.NOT_FOUND, + `Unsupported message 'tree': must be one of ${params.allowedValues?.join(", ")}`, + ); + } + break; + case "additionalProperties": + return createErrorMessage( + ERROR_CATEGORY, + STATUS_ERRORS.NOT_FOUND, + `Unsupported property '${params.additionalProperty}' found`, + ); + default: + return createErrorMessage( + ERROR_CATEGORY, + STATUS_ERRORS.BAD_REQUEST, + `Validation error on field '${instancePath}': ${err.message}.`, + ); + } + + return standardError; + }); +}; + +/** + * Validates a JSON message against predefined schemas. + * + * @param message - The JSON message to be validated. + * @returns The parsed message if valid, otherwise throws an error. + */ +export const validateMessage = (message: string): Message | Error => { + try { + // Try to parse the JSON message + const parsedMessage: Message = JSON.parse(message); + + // Determine the schema key based on the message type + let schemaKey: keyof typeof schemas; + switch (parsedMessage.type) { + case "read": + schemaKey = parsedMessage.nodes ? "multiNodeRead" : "read"; + break; + case "write": + schemaKey = parsedMessage.nodes ? "multiNodeWrite" : "write"; + break; + case "subscribe": + schemaKey = "subscribe"; + break; + case "unsubscribe": + schemaKey = "unsubscribe"; + break; + default: + const error = JSON.stringify([ + createErrorMessage( + "messageValidation", + STATUS_ERRORS.NOT_FOUND, + `Unsupported message type (${parsedMessage.type})`, + ), + ]); + throw new Error(error); + } + + // Validate the parsed message against the schema + const validate = ajv.compile(schemas[schemaKey]); + + if (!validate(parsedMessage)) { + const customError = customizeErrorMessage(validate.errors); + throw new Error(JSON.stringify(customError)); + } + return parsedMessage; + } catch (error: unknown) { + if (error instanceof SyntaxError) { + // Handle JSON parsing error + const jsonError = JSON.stringify([ + createErrorMessage( + "messageValidation", + STATUS_ERRORS.NOT_FOUND, + "The JSON format in the message is not valid.", + ), + ]); + return new Error(jsonError); + } else { + // Handle other errors + const errMsg = error instanceof Error ? error.message : "Unknown error"; + return new Error(errMsg); + } + } +}; diff --git a/cdsp/information-layer/tsconfig.json b/cdsp/information-layer/tsconfig.json new file mode 100644 index 0000000..6a50823 --- /dev/null +++ b/cdsp/information-layer/tsconfig.json @@ -0,0 +1,115 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "." /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, + "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "./**/*.ts", + "./handlers/src/iotdb/gen-nodejs/**/*.js" // Ensure JS files in gen-nodejs are included + ], + "exclude": ["./dist", "node_modules"] +} diff --git a/cdsp/information-layer/utils/error-message-helper.ts b/cdsp/information-layer/utils/error-message-helper.ts new file mode 100644 index 0000000..48d6218 --- /dev/null +++ b/cdsp/information-layer/utils/error-message-helper.ts @@ -0,0 +1,19 @@ +import { ErrorMessage } from "../handlers/utils/data_types"; + +/** + * Creates an error message object with the specified category, status code, and message. + * + * @param category - The category of the error. + * @param statusCode - The HTTP status code associated with the error. + * @param message - A descriptive message explaining the error. + * @returns An ErrorMessage object containing the category, status code, and message. + */ +export const createErrorMessage = ( + category: string, + statusCode: number, + message: string, +): ErrorMessage => ({ + category, + statusCode, + message, +}); diff --git a/cdsp/information-layer/utils/logger.ts b/cdsp/information-layer/utils/logger.ts new file mode 100644 index 0000000..da127d0 --- /dev/null +++ b/cdsp/information-layer/utils/logger.ts @@ -0,0 +1,104 @@ +// Define COLORS as an enum +export enum COLORS { + RESET = "\x1b[0m", + GREEN = "\x1b[32m", + BLUE = "\x1b[34m", + RED = "\x1b[31m", + PALE_WHITE = "\x1b[37m", + GREY = "\x1b[90m", + YELLOW = "\x1b[33m", + BOLD = "\x1b[1m", +} + +// Define the MessageType enum to strictly enforce allowed types +export enum MessageType { + RECEIVED = "RECEIVED", + SENT = "SENT", + ERROR = "ERROR", + WARNING = "WARNING", + OTHER = "OTHER", +} + +// Mapping MessageType to their corresponding colors +const MessageTypeColors: Record = { + [MessageType.RECEIVED]: COLORS.GREEN, + [MessageType.SENT]: COLORS.BLUE, + [MessageType.ERROR]: COLORS.RED, + [MessageType.WARNING]: COLORS.YELLOW, + [MessageType.OTHER]: COLORS.PALE_WHITE, +}; + +/** + * Logs a message to the console with a specified color. + * + * @param message - The message to be logged. + * @param color - The color to apply to the message. Should be a value from the COLORS enum. + */ +export function logWithColor(message: string, color: COLORS): void { + console.log("\n", color, message, COLORS.RESET); +} + +/** + * Logs a message with a specific format including a timestamp, message type, and label. + * + * @param featureStr - The main message string to be logged. + * @param type - The type of the message, which determines the label and color. Defaults to MessageType.OTHER. + * @param label - An optional label to override the default label for the message type. + */ +export function logMessage( + featureStr: string, + type: MessageType = MessageType.OTHER, + label: string = "" +): void { + let color: COLORS = MessageTypeColors[type]; + const dateTimeNow = new Date().toLocaleString(); + let labelText: string; + + switch (type) { + case MessageType.RECEIVED: + labelText = "\u2193 ".concat(label || "Client Received"); + break; + case MessageType.SENT: + labelText = "\u2191 ".concat(label || "Client Sent"); + break; + case MessageType.ERROR: + labelText = "\u2716 ".concat(label || "Internal Error"); + break; + case MessageType.OTHER: + labelText = label || "Message"; + color = COLORS.RESET; + break; + default: + labelText = label || "Unknown"; + color = COLORS.RESET; + } + + const logEntry = `\n${COLORS.PALE_WHITE}${dateTimeNow}${COLORS.RESET} ${color}${labelText}${COLORS.RESET}`; + console.log(logEntry); + console.log(featureStr); +} + +/** + * Logs an error message with a specified feature string. + * + * @param featureStr - A string representing the feature or context where the error occurred. + * @param error - The error object or unknown error to be logged. + * If it's an instance of Error, its message will be logged. + * Otherwise, 'Unknown error' will be logged. + */ +export function logError(featureStr: string, error: Error | unknown): void { + const errMsg = featureStr + .concat(": ") + .concat(error instanceof Error ? error.message : "Unknown error"); + logMessage(errMsg, MessageType.ERROR); +} + +/** + * Logs an error message with a specified error message type. + * + * @param errMsg - The error message to be logged. + * @returns void + */ +export function logErrorStr(errMsg: string): void { + logMessage(errMsg, MessageType.ERROR); +} diff --git a/cdsp/knowledge-layer/.clang-format b/cdsp/knowledge-layer/.clang-format new file mode 100644 index 0000000..88980f1 --- /dev/null +++ b/cdsp/knowledge-layer/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 +AllowShortIfStatementsOnASingleLine: false +SpaceAfterCStyleCast: true diff --git a/cdsp/knowledge-layer/.gitignore b/cdsp/knowledge-layer/.gitignore new file mode 100644 index 0000000..961e9ce --- /dev/null +++ b/cdsp/knowledge-layer/.gitignore @@ -0,0 +1,2 @@ +# CMake build folder +build/ \ No newline at end of file diff --git a/cdsp/knowledge-layer/CMakeLists.txt b/cdsp/knowledge-layer/CMakeLists.txt new file mode 100644 index 0000000..e7e16b9 --- /dev/null +++ b/cdsp/knowledge-layer/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.10) + +project(knowledge-layer) + +# C++ standard +set(CMAKE_CXX_STANDARD 17) + +# Find Boost libraries +find_package(Boost 1.86.0 REQUIRED COMPONENTS system filesystem thread) + +if(WIN32) + include_directories(${Boost_INCLUDE_DIRS}) +endif() + +# FetchContent to include nlohmann/json +include(FetchContent) +FetchContent_Declare( + json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 +) +FetchContent_MakeAvailable(json) + +# Output directory for the executable +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Define the project root directory +set(PROJECT_ROOT_DIR "${CMAKE_SOURCE_DIR}") + +# Pass the root directory to your C++ code +add_compile_definitions(PROJECT_ROOT="${PROJECT_ROOT_DIR}") + +enable_testing() + +# Add subdirectory for the websocket-client +add_subdirectory(connector/websocket-client) +add_subdirectory(symbolic-reasoner/rdfox/rdfox-service-test) +add_subdirectory(symbolic-reasoner/rdfox/tests) \ No newline at end of file diff --git a/cdsp/knowledge-layer/README.md b/cdsp/knowledge-layer/README.md new file mode 100644 index 0000000..f06c6e0 --- /dev/null +++ b/cdsp/knowledge-layer/README.md @@ -0,0 +1,136 @@ +# Knowledge Layer - WebSocket Client + +This project contains a WebSocket client built in C++17, which communicates with a WebSocket server. The client can send and receive JSON messages using Boost libraries. + +## Prerequisites + +Before setting up the project, make sure you have the following installed: + +- **CMake** (version 3.10 or higher) +- **Boost** (version 1.86.0 or higher) +- **g++/clang++** with C++17 support +- **Homebrew** (for macOS users) +- A WebSocket server to connect to, see how to start the **information-layer** Websocket server [here](../information-layer/README.md) + +### Installing Dependencies + +1. **Install g++**: + On windows you may use mingw, e.g. you can install mingw64 with msys64 from here: https://www.msys2.org/ + + Open the MSYS2 MinGW64 Shell. Make sure you're using the MSYS2 MinGW64 Shell (not the regular MSYS2 shell), as this is required for using the 64-bit GCC toolchain. You can find this in the Start menu as MSYS2 MinGW 64-bit. Run the following command: + + ```bash + pacman -Syu # Update MSYS2 packages + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make # Install 64-bit GCC and Make + ``` + After the installation, verify that gcc and g++ are installed and accessible in the MSYS2 MinGW64 shell by running: + + ```bash + gcc --version + g++ --version + ``` + You should see the version information for the installed GCC toolchain. + + +2. **Install Boost**: + If you're on macOS, you can install Boost using Homebrew: + + ```bash + brew install boost + ``` + + For Linux, you can install Boost using your package manager, or build from source: + + ```bash + sudo apt-get install libboost-all-dev + ``` + + On Windows you can download a zipfile or take an installer, e.g. from here: https://sourceforge.net/projects/boost/files/boost-binaries/. To use all functionality you have to build the boost library with the gcc compiler. + + Open the MSYS2 MinGW64 Shell from the Start menu (not the standard MSYS2 shell), navigate to your Boost directory and build: + + ```bash + cd /c/path/to/boost_1_86_0 + ./bootstrap.sh gcc + ./b2 --with-system --with-filesystem --with-thread + ``` + + Important Note: When you want to build the projekt with cmake later, cmake often expects specific library names. So if it does not find the libraries, you have to rename them, e.g.: + + + **libboost_system-mgw14-mt-x64-1_86.a** to **libboost_system.a** for the release version and + **libboost_system-mgw14-mt-d-x64-1_86.a** tp **libboost_system-d.a** for the debug version. + + And the same for filesystem, thread, atomic and chrono. + +3. **Install CMake**: + Ensure you have CMake installed: + + ```bash + brew install cmake # For macOS + sudo apt-get install cmake # For Linux + ``` + + On Windows you can get an installer here: https://cmake.org/download/. + + Make sure you have Windows SDK 10 installed. To check start the Visual Studio Installer (https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) and look for Visual Studio Build Tools. Here look for the component 'Desktop development with C++' and install it if required. + + +## Project Setup + +**Build the WebSocket Client**: + + The WebSocket client is located in the `cdsp/knowledge_layer/` directory. To build it from this directory: + + ```bash + # Create a build directory + mkdir -p build + cd build + + # Run CMake to generate build files + # For Mac: + cmake .. + # For Windows you have to specify the location of the boost libraries: + cmake -DBOOST_ROOT= -DBOOST_LIBRARYDIR=/stage/lib .. + + # Build the project + make + ``` + + If the build is successful, the WebSocket client executable will be generated in the `build/bin/` directory. + +## Running the WebSocket Client + +### Environment Variables + +The project requires certain environment variables to work with the WebSocket server’s host and port. By default, the WebSocket client connects to the `information-layer`. See how to configure these variables [here](../information-layer/README.md). + + +- **HOST_WEBSOCKET_SERVER:** Specifies the hostname of the WebSocket server. The default is `localhost`. +- **PORT_WEBSOCKET_SERVER:** Specifies the port for connecting to the WebSocket server. The default is `8080`. +- **OBJECT_ID:** The object id is required to subscribe and retrieve information for a specific object (e.g. VIN (Vehicle Identification Number) for VSS (Vehicle Signal Specification) data). Use the object id (in this case VIN) configured in the [`information-layer`](../information-layer/README.md). + +You can customize the WebSocket server configuration by adding the following environment variables in the `/docker/.env` file. Below is an example of what the file could look like: + +```text +################################## +# WEBSOCKET-SERVER CONFIGURATION # +################################## + +HOST_WEBSOCKET_SERVER="your_custom_host" +PORT_WEBSOCKET_SERVER="your_custom_port" +OBJECT_ID="OBJECT_ID_TO_SUBSCRIBE" +``` + +### Start the Websocket Client +After successfully building the client, you can run it with the following command (VIN is required, if any docker for the knowledge layer is running): + +```bash +VIN= ./build/bin/websocket_client +``` + +To display a list of available environment variables and their default values, run the application with the `--help` flag: + +```bash +./websocket_client --help +``` \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/README.md b/cdsp/knowledge-layer/connector/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/connector/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/README.md b/cdsp/knowledge-layer/connector/json-rdf-convertor/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/README.md b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/README.md b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/README.md b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/data_structs.h b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/data_structs.h new file mode 100644 index 0000000..eb3eeec --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/data_structs.h @@ -0,0 +1,74 @@ +/// +/// @file +/// @copyright Copyright (C) 2017-2023, Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +/// + +#ifndef DATA_STRUCTS_DATA_STRUCTS_H +#define DATA_STRUCTS_DATA_STRUCTS_H + +#include +#include +#include + +struct MessageHeader +{ + std::string id; + std::string type; + std::string tree; + std::string date_time; + std::string timestamp; + std::chrono::duration time_stamp; +}; + +struct Node +{ + std::string name; + std::string date_time; + std::string timestamp; + std::vector values; + std::chrono::duration time_stamp; +}; + +struct Data +{ + MessageHeader header; + std::vector nodes; +}; + +struct DataPoint +{ + std::string name; + std::string value; +}; + +struct InferredResult +{ + std::string entity_id; + std::vector data; +}; + +struct ProgramOptions +{ + std::string map; + std::string output_ttl; + std::string persist_data; + std::string entity_id; + + bool enable_message_log; + bool test_rules; + bool dummy_server; +}; + +struct QueryData +{ + std::string name_one; + std::string name_two; + std::string has_statement; + std::string data_type; + std::string prefix_name_one; + std::string prefix_name_two; + std::string prefix_data_type; + std::string prefix_has_statement; +}; + +#endif // DATA_STRUCTS_DATA_STRUCTS_H diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/utils.h b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/utils.h new file mode 100644 index 0000000..f163215 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/utils/utils.h @@ -0,0 +1,89 @@ +/// +/// @file +/// @copyright Copyright (C) 2017-2023, Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +/// + +#ifndef UTILS_UTILS_H +#define UTILS_UTILS_H + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "data_structs.h" + +namespace utils +{ +namespace parser +{ +Data GetVehicleFeatureDataFromString(const std::string& string_feature); + +void RemoveChars(const char chars_to_remove[], std::string& string); + +void RemoveSubstring(const std::string& to_erase, std::string& string_feature); + +void SubstituteCharacter(std::string& input, char a, char b); + +Json::Value ParseJsonFromStream(const std::string& input_json); + +Json::Value ParseJsonFromString(const std::string& input_json); + +std::string FindDataTypesInJsonStream(const Json::Value& json_value, + const std::string& name_structure, + std::unordered_map& data_types_map); + +std::chrono::duration GetChronoTimeFromDateTime(const std::string& date_time_string); + +std::chrono::duration> GetChronoTimeInMinutesFromTime(const std::string& time_string); + +std::string GetHourPartFromDateTime(const std::string& date_time); + +std::string GetHourPartFromDateTimeAndSum(const std::string& start_time, const std::string& end_time); + +std::vector ParseWhenMultiNode(const Json::Value& nodes); + +Node ParseWhenSingleNode(const Json::Value& node); + +} // namespace parser + +void WriteStringToFile(const std::string& string_to_write); + +std::vector GetCurrentInferenceResultInJsonFormat(const std::vector& result, + const std::string& type, + const std::string& tree, + const std::string& id, + const bool is_test); + +std::vector ConvertCurrentInferenceResultToJson(const std::vector& result, + const std::string& type, + const std::string& tree, + const std::string& id); + +Json::Value GroupMessages(const Json::Value& message_1, const Json::Value& message2); + + +std::string GetIntials(const std::string input_string); + + +template +std::string to_string_with_precision(const T a_value, const int n = 15) +{ + std::ostringstream out; + out.precision(n); + out << std::fixed << a_value; + return out.str(); +} +} // namespace utils + +#endif diff --git a/cdsp/knowledge-layer/connector/utils/data_types.h b/cdsp/knowledge-layer/connector/utils/data_types.h new file mode 100644 index 0000000..2b4950d --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/data_types.h @@ -0,0 +1,92 @@ +#ifndef DATA_TYPES_DATA_TYPES_H +#define DATA_TYPES_DATA_TYPES_H + +#include +#include +#include +#include +#include + +struct ReasonerSettings { + std::string inference_engine; + std::string output_format; + std::vector supported_tree_types; +}; + +struct ModelConfig { + std::map> system_data_points; + std::string output_file_path; + std::vector ontology_files; + std::vector shacl_shapes_files; + std::map> triple_assembler_queries_files; + std::string output_queries_path; + std::vector rules_files; + ReasonerSettings reasoner_settings; +}; + +struct ServerData { + std::string host; + std::string port; + std::string auth_base64; +}; + +struct InitConfig { + std::string uuid; + ServerData websocket_server; + ServerData rdfox_server; + std::string oid; + ModelConfig model_config; +}; + +struct MessageHeader { + std::string id; + std::string type; + std::string tree; + std::string date_time; + std::string uuid; +}; + +struct Node { + std::string name; + std::string value; +}; + +struct DataMessage { + MessageHeader header; + std::vector nodes; +}; + +struct ErrorNode { + std::string name; + std::string status; +}; + +struct ErrorMessage { + std::string type; + std::variant> error; + int errorCode; +}; + +enum class MessageType { + READ, + WRITE, + SUBSCRIBE, + UNSUBSCRIBE, +}; + +inline std::string messageTypeToString(MessageType type) { + switch (type) { + case MessageType::READ: + return "read"; + case MessageType::WRITE: + return "write"; + case MessageType::SUBSCRIBE: + return "subscribe"; + case MessageType::UNSUBSCRIBE: + return "unsubscribe"; + default: + throw std::runtime_error("Unsupported message type"); + } +} + +#endif // DATA_TYPES_DATA_TYPES_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/utils/helper.cpp b/cdsp/knowledge-layer/connector/utils/helper.cpp new file mode 100644 index 0000000..6df262b --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/helper.cpp @@ -0,0 +1,33 @@ +#include "helper.h" + +#include + +/** + * @brief Converts a given string to lowercase. + * + * This function takes an input string and converts all its characters to lowercase. + * + * @param input The input string to be converted. + * @return A new string with all characters in lowercase. + */ +std::string toLowercase(const std::string& input) { + std::string result = input; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +/** + * @brief Converts a given string to uppercase. + * + * This function takes an input string and converts all its characters to uppercase. + * + * @param input The input string to be converted. + * @return A new string with all characters in uppercase. + */ +std::string toUppercase(const std::string& input) { + std::string result = input; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::toupper(c); }); + return result; +} diff --git a/cdsp/knowledge-layer/connector/utils/helper.h b/cdsp/knowledge-layer/connector/utils/helper.h new file mode 100644 index 0000000..91ecaf5 --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/helper.h @@ -0,0 +1,9 @@ +#ifndef HELPER_H +#define HELPER_H + +#include + +std::string toLowercase(const std::string& input); +std::string toUppercase(const std::string& input); + +#endif // HELPER_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/utils/message_utils.cpp b/cdsp/knowledge-layer/connector/utils/message_utils.cpp new file mode 100644 index 0000000..76d1f22 --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/message_utils.cpp @@ -0,0 +1,194 @@ +#include "message_utils.h" + +/** + * @brief Creates a JSON message header with the specified parameters. + * + * @param type The type of the message. + * @param tree The tree identifier for the message. + * @param id The unique identifier for the message. + * @param uuid The unique identifier for the client. + * @return A JSON object containing the message header. + */ +json createMessageHeader(const MessageType& type, const std::string& tree, const std::string& id, + const std::string& uuid) { + json message_header; + message_header["type"] = messageTypeToString(type); + message_header["tree"] = tree; + message_header["id"] = id; + message_header["uuid"] = uuid; + return message_header; +} + +/** + * @brief Creates a subscription message and adds it to the reply messages queue. + * + * This function generates a subscription message header using the provided + * uuid and oid, and then appends this message to the reply messages queue. + * + * @param uuid Used as a client identifier. + * @param oid The object identification number. + * @param tree The tree identifier for the message. + * @param reply_messages_queue A reference to the queue where the subscription message will be + * added. + */ +void createSubscription(const std::string& uuid, const std::string& oid, const std::string& tree, + std::vector& reply_messages_queue) { + reply_messages_queue.push_back(createMessageHeader(MessageType::SUBSCRIBE, tree, oid, uuid)); +} + +/** + * @brief Creates a read message and appends it to the reply messages queue. + * + * This function constructs a JSON message with a header and a list of data points, + * then appends the message to the provided reply messages queue. + * + * @param uuid The client identifier. + * @param tree The tree identifier for the message. + * @param oid The object identification number. + * @param data_points A vector of data point names to be included in the message. + * @param reply_messages_queue A reference to the queue where the constructed message will be + * appended. + */ +void createReadMessage(const std::string& uuid, const std::string& tree, const std::string& oid, + const std::vector& data_points, + std::vector& reply_messages_queue) { + auto message = createMessageHeader(MessageType::READ, tree, oid, uuid); + + json nodes = json::array(); + for (const auto& data_point : data_points) { + json node; + node["name"] = data_point; + nodes.push_back(node); + } + message["nodes"] = nodes; + reply_messages_queue.push_back(std::move(message)); +} + +/** + * @brief Parses a JSON message to extract error information and constructs an ErrorMessage object. + * + * This function handles the extraction of error details from a JSON message. The "error" field in + * the JSON message can either be a string or an object. If it is an object, it can contain either a + * single "node" or multiple "nodes". The function constructs the appropriate ErrorMessage object + * based on the content of the JSON message. + * + * @param json_message The JSON message containing error information. + * @return ErrorMessage The constructed ErrorMessage object containing the parsed error details. + */ +ErrorMessage parseErrorMessage(const json& json_message) { + ErrorMessage error_message; + + error_message.type = json_message["type"].get(); + error_message.errorCode = json_message["errorCode"].get(); + + // Handle the "error" field which could be either a string or an object + if (json_message["error"].is_string()) { + error_message.error = json_message["error"].get(); + } else if (json_message["error"].is_object()) { + const auto json_error = json_message["error"]; + if (json_error.contains("node")) { + ErrorNode error_node; + error_node.name = json_error["node"]["name"].get(); + error_node.status = json_error["node"]["status"].get(); + std::vector error_nodes = {error_node}; + error_message.error = error_nodes; + + } else if (json_error.contains("nodes")) { + std::vector error_nodes; + for (const auto& node_item : json_error["nodes"]) { + ErrorNode error_node; + error_node.name = node_item["name"].get(); + error_node.status = node_item["status"].get(); + error_nodes.push_back(error_node); + } + error_message.error = error_nodes; + } + } + + return error_message; +} + +/** + * @brief Converts a node value of a JSON object to its string representation. + * + * This function takes a JSON value of a node and converts it to a string based on its type. + * It supports string, floating-point number, integer, and unsigned integer types. + * If the JSON value is of an unsupported type, it throws a runtime error. + * + * @param json_value The JSON node value to be converted to a string. + * @return A string representation of the JSON value. + * @throws std::runtime_error If the JSON value is of an unsupported type. + */ +std::string nodeValueToString(const json& json_value) { + if (json_value.is_string()) { + return json_value.get(); + } else if (json_value.is_number_float()) { + return std::to_string(json_value.get()); + } else if (json_value.is_number_integer()) { + return std::to_string(json_value.get()); + } else if (json_value.is_number_unsigned()) { + return std::to_string(json_value.get()); + } + throw std::runtime_error("The message contains a node with an unsupported value."); +} + +/** + * @brief Parses a JSON message and constructs a DataMessage object. + * + * This function extracts the header information and node(s) from the provided JSON message + * and constructs a DataMessage object. The JSON message is expected to contain specific fields + * such as "id", "type", "tree", "dateTime", and "uuid" for the header. It may also contain either + * a single "node" or multiple "nodes". + * + * @param json_message The JSON message to be parsed. + * @return DataMessage The constructed DataMessage object containing the parsed data. + */ +DataMessage parseSuccessMessage(const json& json_message) { + MessageHeader header; + + header.id = json_message.at("id").get(); + header.type = json_message.at("type").get(); + header.tree = json_message.at("tree").get(); + header.date_time = json_message.at("dateTime").get(); + header.uuid = json_message.at("uuid").get(); + + DataMessage data_message; + + data_message.header = header; + if (json_message.contains("node")) { + Node node; + node.name = json_message["node"].at("name").get(); + node.value = nodeValueToString(json_message["node"].at("value")); + data_message.nodes.push_back(node); + } else if (json_message.contains("nodes")) { + for (const auto& node_item : json_message["nodes"]) { + Node node; + node.name = node_item.at("name").get(); + node.value = nodeValueToString(node_item.at("value")); + data_message.nodes.push_back(node); + } + } + + return data_message; +} + +/** + * @brief Parses and displays a JSON message, returning either a DataMessage or an ErrorMessage. + * + * This function takes a JSON string, parses it, and prints the parsed JSON to the standard output. + * Depending on the content of the JSON, it will return either a DataMessage or an ErrorMessage. + * + * @param message The JSON string to be parsed and displayed. + * @return std::variant The parsed message, which can be either a + * DataMessage or an ErrorMessage. + */ +std::variant displayAndParseMessage(const std::string& message) { + json json_message = json::parse(message); + std::cout << "Received message: " << json_message.dump() << std::endl << std::endl; + + if (json_message.contains("error")) { + return parseErrorMessage(json_message); + } + + return parseSuccessMessage(json_message); +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/utils/message_utils.h b/cdsp/knowledge-layer/connector/utils/message_utils.h new file mode 100644 index 0000000..268949f --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/message_utils.h @@ -0,0 +1,32 @@ +#ifndef MESSAGE_UTILS_H +#define MESSAGE_UTILS_H + +#include +#include +#include +#include +#include + +#include "data_types.h" + +using json = nlohmann::json; + +json createMessageHeader(const std::string& type, const std::string& tree, const std::string& id, + const std::string& uuid); + +void createSubscription(const std::string& uuid, const std::string& oid, const std::string& tree, + std::vector& reply_messages_queue); + +void createReadMessage(const std::string& uuid, const std::string& tree, const std::string& oid, + const std::vector& data_points, + std::vector& reply_messages_queue); + +ErrorMessage parseErrorMessage(const json& json_message); + +std::string nodeValueToString(const json& json_value); + +DataMessage parseSuccessMessage(const json& json_message); + +std::variant displayAndParseMessage(const std::string& message); + +#endif // MESSAGE_UTILS_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/utils/model_config.cpp b/cdsp/knowledge-layer/connector/utils/model_config.cpp new file mode 100644 index 0000000..cb985db --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/model_config.cpp @@ -0,0 +1,193 @@ +#include + +#include +#include + +#include "helper.h" +/** + * @brief Reads a file and returns a list of required data points. + * + * This function constructs the full path to the file using the provided file name + * and a predefined project root directory. It then reads the file line by line, + * storing each line as an element in a vector of strings. + * + * @param file_name The name of the file to read. + * @return A vector of strings, each representing a required data point from the file. + * @throws std::runtime_error if the file cannot be opened. + */ +std::vector getClientRequiredDataPoints(std::string file_name) { + std::vector required_data; + std::string root = + std::string(PROJECT_ROOT) + "/symbolic-reasoner/examples/use-case/model/" + file_name; + std::ifstream file(root); + if (!file) { + throw std::runtime_error("Invalid required Data Points file: " + file_name); + } + std::string line; + while (std::getline(file, line)) { + required_data.push_back(line); + } + return required_data; +} + +/** + * @brief Validates the presence of required fields in a JSON configuration. + * + * This function checks for the existence of specific top-level fields and nested fields + * within the provided JSON object. If any required field is missing, it throws a runtime error + * with a descriptive message indicating which field is missing. + * + * @param config_json The JSON object representing the configuration to be validated. + * + * @throws std::runtime_error If any required field is missing in the JSON configuration. + */ +void validateJsonFields(const json& config_json) { + const std::string generic_error_message = + "Error in the model_config.json file. Missing required field"; + // Required top-level fields + if (!config_json.contains("inputs")) { + throw std::runtime_error(generic_error_message + ": 'inputs'"); + } + if (!config_json.contains("ontologies")) { + throw std::runtime_error(generic_error_message + ": 'ontologies'"); + } + if (!config_json.contains("output")) { + throw std::runtime_error(generic_error_message + ": 'output'"); + } + if (!config_json.contains("queries")) { + throw std::runtime_error(generic_error_message + ": 'queries'"); + } + if (!config_json.contains("rules")) { + throw std::runtime_error(generic_error_message + ": 'rules'"); + } + if (!config_json.contains("shacl")) { + throw std::runtime_error(generic_error_message + ": 'shacl'"); + } + if (!config_json.contains("reasoner_settings")) { + throw std::runtime_error(generic_error_message + " 'reasoner_settings'"); + } + + // Validate structure inside "queries" + const auto& queries = config_json["queries"]; + if (!queries.contains("triple_assembler_helper")) { + throw std::runtime_error(generic_error_message + + " in 'queries': 'triple_assembler_helper'"); + } + if (!queries.contains("output")) { + throw std::runtime_error(generic_error_message + " in 'queries': 'output'"); + } + + // Validate structure inside "reasoner_settings" + const auto& reasoner_settings = config_json["reasoner_settings"]; + if (!reasoner_settings.contains("inference_engine")) { + throw std::runtime_error(generic_error_message + + " in 'reasoner_settings': 'inference_engine'"); + } + if (!reasoner_settings.contains("output_format")) { + throw std::runtime_error(generic_error_message + + " in 'reasoner_settings': 'output_format'"); + } + if (!reasoner_settings.contains("supported_tree_types")) { + throw std::runtime_error(generic_error_message + + " in 'reasoner_settings': 'supported_tree_types'"); + } +} + +/** + * @brief Loads the model configuration from a JSON file. + * + * This function reads the model configuration from the specified JSON file and populates + * the provided ModelConfig object with the configuration data. It validates the structure + * of the JSON file and ensures that all required fields are present. + * + * @param config_file The path to the JSON configuration file. + * @param model_config The ModelConfig object to be populated with the configuration data. + * + * @throws std::runtime_error If the configuration file cannot be opened or if required fields are + * missing. + */ +void loadModelConfig(const std::string& config_file, ModelConfig& model_config) { + std::ifstream file(config_file); + if (!file) { + throw std::runtime_error("Could not open the model config file: " + config_file); + } + + json config_json; + file >> config_json; + + // Validate the structure of the JSON + validateJsonFields(config_json); + + if (config_json["reasoner_settings"]["supported_tree_types"].size() > 0) { + const std::string generic_error_message = + "Error in the model_config.json file. Missing required field"; + + for (const auto& tree_type : config_json["reasoner_settings"]["supported_tree_types"]) { + std::string lc_tree_type = toLowercase(tree_type.get()); + + // Read supported tree types + model_config.reasoner_settings.supported_tree_types.push_back( + toUppercase(tree_type.get())); + + // Read system data points for a tree type + if (!config_json["inputs"].contains(lc_tree_type + "_data")) { + throw std::runtime_error(generic_error_message + " in 'inputs': '" + lc_tree_type + + "_data'"); + } + model_config.system_data_points[lc_tree_type] = getClientRequiredDataPoints( + config_json["inputs"][lc_tree_type + "_data"].get()); + + // Read tree type specific triple assembler helpers + if (config_json["queries"]["triple_assembler_helper"].contains(lc_tree_type)) { + std::vector query_list; + for (const auto& query : + config_json["queries"]["triple_assembler_helper"][lc_tree_type]) { + query_list.push_back(query.get()); + } + model_config.triple_assembler_queries_files[lc_tree_type] = query_list; + } else { + std::cout + << (" ** INFO: There is any triple assembler helper for the tree type: '" + + lc_tree_type + "' configured in the model_config.json") + << std::endl + << std::endl; + } + } + } else { + throw std::runtime_error("You need to add some supported tree types in " + config_file); + } + + // Read output file path + model_config.output_file_path = config_json["output"].get(); + + // Read ontologies files + for (const auto& ontology : config_json["ontologies"]) { + model_config.ontology_files.push_back(ontology.get()); + } + + // Read SHACL shapes files + for (const auto& shacl : config_json["shacl"]) { + model_config.shacl_shapes_files.push_back(shacl.get()); + } + + // Read default triple assembler helpers + std::vector query_list; + for (const auto& query : config_json["queries"]["triple_assembler_helper"]["default"]) { + query_list.push_back(query.get()); + } + model_config.triple_assembler_queries_files["default"] = query_list; + + // Read rules paths + for (const auto& rule : config_json["rules"]) { + model_config.rules_files.push_back(rule.get()); + } + + // Read output queries path + model_config.output_queries_path = config_json["queries"]["output"].get(); + + // Read reasoner settings + model_config.reasoner_settings.inference_engine = + config_json["reasoner_settings"]["inference_engine"].get(); + model_config.reasoner_settings.output_format = + config_json["reasoner_settings"]["output_format"].get(); +} diff --git a/cdsp/knowledge-layer/connector/utils/model_config.h b/cdsp/knowledge-layer/connector/utils/model_config.h new file mode 100644 index 0000000..98f866c --- /dev/null +++ b/cdsp/knowledge-layer/connector/utils/model_config.h @@ -0,0 +1,13 @@ +#ifndef MESSAGE_MODEL_CONFIG_H +#define MESSAGE_MODEL_CONFIG_H + +#include + +#include "data_types.h" + +using json = nlohmann::json; + +void validateJsonFields(const json& config_json); +void loadModelConfig(const std::string& config_file, ModelConfig& model_config); + +#endif // MESSAGE_MODE_CONFIG_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt b/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt new file mode 100644 index 0000000..ab27158 --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt @@ -0,0 +1,22 @@ +# Define the WebSocket client target +add_executable( + websocket_client main.cpp + websocket_client.cpp + ../utils/message_utils.cpp + ../utils/helper.cpp + ../utils/model_config.cpp +) + +# Link libraries +target_link_libraries(websocket_client Boost::system Boost::filesystem Boost::thread) +target_link_libraries(websocket_client nlohmann_json::nlohmann_json) +target_include_directories(websocket_client PRIVATE ${CMAKE_SOURCE_DIR}/connector/utils) + + +# On Windows link winsock, too +if(WIN32) + target_link_libraries(websocket_client ws2_32) +endif() + +# Use defined output directory for the executable +set_target_properties(websocket_client PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/README.md b/cdsp/knowledge-layer/connector/websocket-client/README.md new file mode 100644 index 0000000..17efc5b --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/README.md @@ -0,0 +1,3 @@ +# Websocket-Client + +This project starts a [WebSocket client](main.cpp) that connects to a WebSocket server, sends JSON messages, and handles incoming responses. The client dynamically reads the server’s host and port from the environment variables or falls back to default values if none are provided. diff --git a/cdsp/knowledge-layer/connector/websocket-client/main.cpp b/cdsp/knowledge-layer/connector/websocket-client/main.cpp new file mode 100644 index 0000000..ed0cbeb --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/main.cpp @@ -0,0 +1,114 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "websocket_client.h" + +using json = nlohmann::json; + +boost::uuids::random_generator uuidGenerator; + +std::string DefaultHostWebsocketServer{"127.0.0.1"}; +std::string DefaultPortWebSocketServer{"8080"}; +std::string ModelConfigurationFile{std::string(PROJECT_ROOT) + + "/symbolic-reasoner/examples/use-case/model/model_config.json"}; +std::string DefaultRDFoxServer{"127.0.0.1"}; +std::string DefaultPortRDFoxServer{"12110"}; +std::string DefaultAuthRDFoxServerBase64{"cm9vdDphZG1pbg=="}; // For 'root:admin' encoded in base64 + +std::string getEnvVariable(const std::string& envVar, const std::string& defaultValue = "") { + const char* valueEnv = std::getenv(envVar.c_str()); + return valueEnv ? std::string(valueEnv) : defaultValue; +} + +void displayHelp() { + std::string bold = "\033[1m"; + std::string lightRed = "\033[91m"; + std::string reset = "\033[0m"; + + std::cout << bold << "Usage: websocket_client [--help]\n\n" << reset; + + std::cout << "This table contains a lists environment variables set for the WebSocket client " + "and their descriptions.\n\n"; + // Table header + std::cout << bold << std::left << std::setw(35) << "Variable" << std::setw(65) << "Description" + << "Value" << reset << "\n"; + std::cout << std::string(140, '-') << "\n"; // Line separator + + std::cout << std::left << std::setw(35) << "HOST_WEBSOCKET_SERVER" << std::setw(65) + << "IP address of the WebSocket server" + << getEnvVariable("HOST_WEBSOCKET_SERVER", DefaultHostWebsocketServer) << "\n"; + std::cout << std::left << std::setw(35) << "PORT_WEBSOCKET_SERVER" << std::setw(65) + << "Port number of the WebSocket server" + << getEnvVariable("PORT_WEBSOCKET_SERVER", DefaultPortWebSocketServer) << "\n"; + std::cout << std::left << std::setw(35) << "OBJECT_ID" << std::setw(65) + << "Object ID to be used in communication" + << getEnvVariable("OBJECT_ID", lightRed + "`Not Set (Required)`" + reset) << "\n"; + std::cout << std::left << std::setw(35) << "HOST_RDFOX_SERVER" << std::setw(65) + << "IP address of the RDFox server" + << getEnvVariable("HOST_RDFOX_SERVER", DefaultRDFoxServer) << "\n"; + std::cout << std::left << std::setw(35) << "PORT_RDFOX_SERVER" << std::setw(65) + << "Port number of the RDFox server" + << getEnvVariable("PORT_RDFOX_SERVER", DefaultPortRDFoxServer) << "\n"; + std::cout << std::left << std::setw(35) << "AUTH_RDFOX_SERVER_BASE64" << std::setw(65) + << "Authentication credentials for RDFox Server encoded in base64" + << getEnvVariable("AUTH_RDFOX_SERVER_BASE64", DefaultAuthRDFoxServerBase64) << "\n"; + + std::cout << "\n" << bold << "Description:\n" << reset; + std::cout << "This client connects to a WebSocket server and processes incoming messages based " + "on the defined input.\n"; + std::cout << "The above environment variables are used to configure the application.\n\n"; +} + +/** + * @brief Initializes and returns an InitConfig object with required configuration variables. + * + * This function creates an InitConfig instance and retrieving values + * from environment variables. If the environment variables are not set, + * default values are used. + * + * @return InitConfig The initialized configuration object. + * @throws std::runtime_error if there is a problem setting a variable. + */ +InitConfig AddInitConfig() { + ModelConfig model_config; + loadModelConfig(ModelConfigurationFile, model_config); + + InitConfig init_config; + init_config.websocket_server.host = + getEnvVariable("HOST_WEBSOCKET_SERVER", DefaultHostWebsocketServer); + init_config.websocket_server.port = + getEnvVariable("PORT_WEBSOCKET_SERVER", DefaultPortWebSocketServer); + init_config.uuid = boost::uuids::to_string(uuidGenerator()); + init_config.oid = getEnvVariable("OBJECT_ID"); + init_config.model_config = model_config; + init_config.rdfox_server.host = getEnvVariable("HOST_RDFOX_SERVER"); + init_config.rdfox_server.port = getEnvVariable("PORT_RDFOX_SERVER"); + init_config.rdfox_server.auth_base64 = getEnvVariable("AUTH_RDFOX_SERVER_BASE64"); + return init_config; +} + +int main(int argc, char* argv[]) { + // Check for --help flag + if (argc > 1 && std::string(argv[1]) == "--help") { + displayHelp(); + return EXIT_SUCCESS; + } + try { + InitConfig init_config = AddInitConfig(); + std ::cout << "** Starting client! **" << std::endl; + auto client = std::make_shared(init_config); + client->run(); + + return EXIT_SUCCESS; + } catch (const std::exception& e) { + std::cout << "Error: " << e.what() << std::endl; + return EXIT_FAILURE; + } +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/websocket_client.cpp b/cdsp/knowledge-layer/connector/websocket-client/websocket_client.cpp new file mode 100644 index 0000000..af9f2ad --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/websocket_client.cpp @@ -0,0 +1,141 @@ +#include "websocket_client.h" + +#include +#include + +#include "helper.h" + +namespace { +void Fail(beast::error_code error_code, const std::string& what) { + std::cerr << what << ": " << error_code.message() << "\n"; +} + +std::string createLogErrorMessage(ErrorMessage& error) { + std::string errorDetails; + if (std::holds_alternative(error.error)) { + errorDetails = std::get(error.error); + } else { + const std::vector& nodes = std::get>(error.error); + for (const auto& node : nodes) { + errorDetails += "\n - Node: " + node.name + ": " + node.status; + } + } + + return "Received message with error code " + std::to_string(error.errorCode) + " - (" + + error.type + ") " + errorDetails; +} +} // namespace + +WebSocketClient::WebSocketClient(const InitConfig& init_config) + : resolver_(io_context_), ws_(io_context_), init_config_(init_config) {} + +void WebSocketClient::run() { + resolver_.async_resolve( + init_config_.websocket_server.host, init_config_.websocket_server.port, + beast::bind_front_handler(&WebSocketClient::onResolve, shared_from_this())); + + // Run the io_context to process asynchronous events + io_context_.run(); +} + +void WebSocketClient::onResolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) { + return Fail(ec, "Resolve failed:"); + } + net::async_connect(ws_.next_layer(), results.begin(), results.end(), + beast::bind_front_handler(&WebSocketClient::onConnect, shared_from_this())); +} + +void WebSocketClient::onConnect(boost::system::error_code ec, + tcp::resolver::iterator /*end_point*/) { + if (ec) { + return Fail(ec, "Connection failed:"); + } + + ws_.async_handshake(init_config_.websocket_server.host, "/", + beast::bind_front_handler(&WebSocketClient::handshake, shared_from_this())); +} + +void WebSocketClient::handshake(beast::error_code ec) { + if (ec) { + return Fail(ec, "Handshake failed:"); + } + std::cout << "Connected to WebSocket server: " << init_config_.websocket_server.host + << std::endl + << std::endl; + + for (const std::string& tree_type : + init_config_.model_config.reasoner_settings.supported_tree_types) { + createSubscription(init_config_.uuid, init_config_.oid, tree_type, reply_messages_queue_); + createReadMessage(init_config_.uuid, tree_type, init_config_.oid, + init_config_.model_config.system_data_points[toLowercase(tree_type)], + reply_messages_queue_); + } + + writeReplyMessagesOnQueue(); +} + +void WebSocketClient::sendMessage(const json& message) { + ws_.async_write(net::buffer(message.dump()), + beast::bind_front_handler(&WebSocketClient::onSendMessage, shared_from_this())); +} + +void WebSocketClient::onSendMessage(boost::system::error_code ec, std::size_t bytes_transferred) { + if (ec) { + return Fail(ec, "write"); + } + std::cout << "Message send! " << bytes_transferred << " bytes transferred" << std::endl + << std::endl; + receiveMessage(); +} + +void WebSocketClient::receiveMessage() { + ws_.async_read( + buffer_, beast::bind_front_handler(&WebSocketClient::onReceiveMessage, shared_from_this())); +} + +void WebSocketClient::onReceiveMessage(beast::error_code ec, std::size_t bytes_transferred) { + if (ec) { + return Fail(ec, "read"); + } + + const auto received_message = + std::make_shared(boost::beast::buffers_to_string(buffer_.data())); + buffer_.consume(bytes_transferred); // Clear the buffer for the next message + processMessage(received_message); +} + +void WebSocketClient::processMessage(std::shared_ptr const& message) { + boost::asio::post( + ws_.get_executor(), + beast::bind_front_handler(&WebSocketClient::onProcessMessage, shared_from_this(), message)); +} + +void WebSocketClient::onProcessMessage(std::shared_ptr const& message) { + response_messages_queue_.push_back(message); + + std::string prio_message = *response_messages_queue_.begin()->get(); + response_messages_queue_.erase(response_messages_queue_.begin()); + + const auto parsed_message = displayAndParseMessage(prio_message); + + if (std::holds_alternative(parsed_message)) { + ErrorMessage error = std::get(parsed_message); + throw std::runtime_error(createLogErrorMessage(error)); + } + + DataMessage data_message = std::get(parsed_message); + + if (!reply_messages_queue_.empty()) { + writeReplyMessagesOnQueue(); + } else { + receiveMessage(); + } +} + +void WebSocketClient::writeReplyMessagesOnQueue() { + json reply_message = reply_messages_queue_.front(); + reply_messages_queue_.erase(reply_messages_queue_.begin()); + std::cout << "Sending queue message: " << reply_message.dump() << std::endl; + sendMessage(reply_message); +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/websocket_client.h b/cdsp/knowledge-layer/connector/websocket-client/websocket_client.h new file mode 100644 index 0000000..8a8922a --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/websocket_client.h @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_types.h" +#include "message_utils.h" + +namespace beast = boost::beast; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = net::ip::tcp; +using json = nlohmann::json; + +class WebSocketClient : public std::enable_shared_from_this { + public: + WebSocketClient(const InitConfig& init_config); + void run(); + void sendMessage(const json& message); + + private: + void onResolve(beast::error_code error_code, tcp::resolver::results_type results); + void onConnect(boost::system::error_code error_code, tcp::resolver::iterator end_point); + void handshake(beast::error_code error_code); + void receiveMessage(); + void onReceiveMessage(beast::error_code error_code, std::size_t bytes_transferred); + void onSendMessage(boost::system::error_code ec, std::size_t bytes_transferred); + void processMessage(std::shared_ptr const& ss); + void onProcessMessage(std::shared_ptr const& ss); + + void writeReplyMessagesOnQueue(); + + std::vector reply_messages_queue_{}; + std::vector> response_messages_queue_{}; + net::io_context io_context_; + websocket::stream ws_; + tcp::resolver resolver_; + beast::flat_buffer buffer_; + + InitConfig init_config_; +}; \ No newline at end of file diff --git a/cdsp/knowledge-layer/numeric-analyzer/README.md b/cdsp/knowledge-layer/numeric-analyzer/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/numeric-analyzer/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/README.md b/cdsp/knowledge-layer/symbolic-reasoner/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/README.md b/cdsp/knowledge-layer/symbolic-reasoner/examples/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/README.md b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/README.md new file mode 100644 index 0000000..9aa12bd --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/README.md @@ -0,0 +1,106 @@ +Here is a sample `README.md` that explains the structure and usage of the `model_config.json` configuration file. + +--- + +# Model Configuration for Reasoner Application + +This `model_config.json` file provides the configuration necessary to run the reasoner application. The file defines the required inputs, ontologies, output settings, queries, rules, SHACL validation, and reasoning engine options. This document explains the purpose of each section in the configuration. + +## Configuration Overview + +The `model_config.json` is structured to provide: +- Input data points +- Ontology files for the domain schema +- SHACL shapes for data validation +- SPARQL queries for data manipulation +- Reasoning rules +- Reasoner settings +- Output configurations + +### Configuration Sections + +#### Inputs +```json +"inputs": { + "vss_data": "inputs/vss_data_required.txt" +} +``` +- **vss_data**: The field must be called `_data`, in the example we use the VSS tree type (`vss_data`). This field specifies the path to the input data file that contains vehicle signals or data points. This file lists the signals that the reasoner application will work with. It supports a TXT format for defining the data points. + +#### Ontologies +```json +"ontologies": ["ontologies/example_ontology.ttl"] +``` +- **ontologies**: A list of ontology files in Turtle format (.ttl). These define the concepts, relationships, and structure of the data in a semantic format that the reasoner will use to understand and process the input data. + +#### Output +```json +"output": "output/generated_triples.ttl" +``` +- **output**: Defines the path to the file where the reasoner will store the results of the inference process. The output will be generated in the format defined in the [reasoner settings](#reasoner-settings) (in this case a turtle `.ttl`). + +#### Queries +```json +"queries": { + "triple_assembler_helper": { + "vss": [ + "queries/triple_assembler_helper/vss/data_property.rq", + "queries/triple_assembler_helper/vss/object_property.rq" + ], + "default": [ + "queries/triple_assembler_helper/default/data_property.rq", + "queries/triple_assembler_helper/default/object_property.rq" + ] + }, + "output": ["queries/output/"] +} +``` +- **queries**: This section includes SPARQL queries that will be used during the reasoning process to assemble triples or extract results from the reasoned data. + - **triple_assembler_helper**: + Queries specifically designed to assemble data points related to an specific data tree (in this case Vehicle Signal Specification (VSS)) or other specifications used by default, if the tree is not defined, including queries for data and object properties. + - **output**: Queries to retrieve the final inference results after the reasoning process. The queries in this section will typically extract insights from the generated triples. + +#### Rules +```json +"rules": ["rules/insight_rules.ttl"] +``` +- **rules**: A list of rule files in Turtle format. These rules define additional logic for the reasoner to apply during inference. The rules may include custom inferences or insights that the reasoner should derive based on the input data. + +#### SHACL Validation +```json +"shacl": [ + "shacl/vss_shacl.ttl", + "shacl/observation_shacl.ttl" +] +``` +- **shacl**: A list of SHACL (Shapes Constraint Language) files that define the constraints and validation rules for the data. SHACL shapes ensure that the input data conforms to specific structural and semantic rules before it is processed by the reasoner. + +#### Reasoner Settings +```json +"reasoner_settings": { + "inference_engine": "RDFox", + "output_format": "turtle", + "supported_tree_types": ["VSS"] +} +``` +- **reasoner_settings**: Configuration options for the reasoning engine. + - **inference_engine**: Specifies the reasoner to use (in this case, RDFox). + > [!NOTE] Supported engines in this repository + > - `RDFox` + + - **output_format**: Defines the format in which the output will be serialized. The current setting is `turtle` for Turtle format. + > [!NOTE] Supported formats in this repository + > - `turtle` for .ttl files + + - **supported_tree_types**: This field defines the types of data trees that the reasoner application can handle. For the communication with the WebSocket server and definition of the input data, it is required to specify how the tree type will be represented. This tree type will be sent as part of the message header during data transfer and is crucial for reading, interpreting the [input data](#inputs) for the reasoner model, and if exist, specific [triple assembler helpers](#queries). With this tree type definition, the reasoner can ensure compatibility with the input data and properly interpret the data points being processed. + > [!NOTE] Supported tree types this repository + > - `VSS` (Vehicle Signal Specification) + +### Example Usage + +Once the `model_config.json` is configured, the reasoner application will: +1. Read the **input data** from the specified file. +2. Validate the data using the **ontologies** and **SHACL shapes**. +3. Apply the **rules** during inference. +4. Use the **queries** to assemble triples and extract results. +5. Generate an **output** file containing the reasoned data in Turtle format. diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/inputs/vss_data_required.txt b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/inputs/vss_data_required.txt new file mode 100644 index 0000000..9fba727 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/inputs/vss_data_required.txt @@ -0,0 +1,7 @@ +Vehicle.Chassis.SteeringWheel.Angle +Vehicle.CurrentLocation.Latitude +Vehicle.CurrentLocation.Longitude +Vehicle.Powertrain.TractionBattery.NominalVoltage +Vehicle.Powertrain.TractionBattery.StateOfCharge.CurrentEnergy +Vehicle.Powertrain.Transmission.CurrentGear +Vehicle.Speed \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/model_config.json b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/model_config.json new file mode 100644 index 0000000..7012fd2 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/model_config.json @@ -0,0 +1,24 @@ +{ + "inputs": { + "vss_data": "inputs/vss_data_required.txt" + }, + "ontologies": ["ontologies/example_ontology.ttl"], + "output": "output/", + "queries": { + "triple_assembler_helper": { + "vss": [ + "queries/triple_assembler_helper/vss/data_property.rq", + "queries/triple_assembler_helper/vss/object_property.rq" + ], + "default": [] + }, + "output": "queries/output/" + }, + "rules": ["rules/insight_rules.ttl"], + "shacl": ["shacl/vss_shacl.ttl", "shacl/observation_shacl.ttl"], + "reasoner_settings": { + "inference_engine": "RDFox", + "output_format": "turtle", + "supported_tree_types": ["vss"] + } +} diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/ontologies/example_ontology.ttl b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/ontologies/example_ontology.ttl new file mode 100644 index 0000000..b44f039 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/ontologies/example_ontology.ttl @@ -0,0 +1 @@ +#exmaple \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/output/generated_triples.ttl b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/output/generated_triples.ttl new file mode 100644 index 0000000..3542e5d --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/output/generated_triples.ttl @@ -0,0 +1,72 @@ +bmw:VehicleWBY11CF080CH470711 a bmw:Vehicle. + + +bmw:ChassisWBY11CF080CH470711 a bmw:Chassis. + +bmw:VehicleWBY11CF080CH470711 bmw:hasPart bmw:ChassisWBY11CF080CH470711. + +bmw:SteeringWheelWBY11CF080CH470711 a bmw:SteeringWheel. +bmw:VehicleWBY11CF080CH470711 bmw:hasPart bmw:SteeringWheelWBY11CF080CH470711. + +bmw:CurrentLocationWBY11CF080CH470711 a bmw:CurrentLocation. +bmw:VehicleWBY11CF080CH470711 bmw:hasSignal bmw:CurrentLocationWBY11CF080CH470711. + + +bmw:PowertrainWBY11CF080CH470711 a bmw:Powertrain. + +bmw:VehicleWBY11CF080CH470711 bmw:hasPart bmw:PowertrainWBY11CF080CH470711. + +bmw:TransmissionWBY11CF080CH470711 a bmw:Transmission. + +bmw:PowertrainWBY11CF080CH470711 bmw:hasPart bmw:TransmissionWBY11CF080CH470711. + +bmw:TractionBatteryWBY11CF080CH470711 a bmw:TractionBattery. + +bmw:PowertrainWBY11CF080CH470711 bmw:hasPart bmw:TractionBatteryWBY11CF080CH470711. + +bmw:StateOfChargeWBY11CF080CH470711 a bmw:StateOfCharge. + +bmw:TractionBatteryWBY11CF080CH470711 bmw:hasSignal bmw:StateOfChargeWBY11CF080CH470711. + + +bmw:Observation20181116155027 a sosa:Observation ; # observation_dateTime + sosa:hasFeatureOfInterest bmw:CurrentLocationWBY11CF080CH470711 ; #last second element of the the data point + sosa:hasSimpleResult "30"^^xsd:double ; # value + sosa:observedProperty bmw:latitude ; #last element of the data point + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime .#dateTime + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:CurrentLocationWBY11CF080CH470711 ; + sosa:hasSimpleResult "50"^^xsd:double ; + sosa:observedProperty bmw:longitude ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:VehicleWBY11CF080CH470711 ; + sosa:hasSimpleResult "50"^^xsd:int ; + sosa:observedProperty bmw:speed ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:SteeringWheelWBY11CF080CH470711 ; + sosa:hasSimpleResult "10"^^xsd:int ; + sosa:observedProperty bmw:angle ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:TransmissionWBY11CF080CH470711 ; + sosa:hasSimpleResult "10"^^xsd:int ; + sosa:observedProperty bmw:currentGear ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:StateOfChargeWBY11CF080CH470711 ; + sosa:hasSimpleResult "98.6"^^xsd:float ; + sosa:observedProperty bmw:CurrentEnergy ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . + +bmw:Observation20181116155027 a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:TractionBatteryWBY11CF080CH470711 ; + sosa:hasSimpleResult "55"^^xsd:int ; + sosa:observedProperty bmw:NominalVoltage ; + sosa:phenomenonTime "2018-11-16 15:50:27"^^xsd:dateTime . \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_data_property.rq b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_data_property.rq new file mode 100644 index 0000000..d62a449 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_data_property.rq @@ -0,0 +1,13 @@ +prefix sh: +prefix val: + + SELECT ?class1 ?data_property ?datatype + WHERE { + val:ObservationShape sh:targetClass ?class1; + sh:property [ + sh:path ?data_property; + sh:datatype ?datatype + ]. + } + + diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_object_property.rq b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_object_property.rq new file mode 100644 index 0000000..578d806 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/default/query_observation_object_property.rq @@ -0,0 +1,20 @@ +prefix pdmo: +prefix bmw: +prefix middleware: +prefix sh: +prefix val: +prefix xsd: +prefix ut: + + +SELECT ?class1 ?object_property +WHERE { + val:ObservationShape sh:targetClass ?class1; + sh:property [ + sh:path ?object_property; + sh:nodeKind sh:IRI + ]. + + } + + diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_data_property.rq b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_data_property.rq new file mode 100644 index 0000000..11d85e2 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_data_property.rq @@ -0,0 +1,21 @@ +prefix pdmo: +prefix bmw: +prefix middleware: +prefix sh: +prefix val: +prefix xsd: +prefix ut: + + + SELECT ?class1 ?data_property ?datatype + WHERE { + ?S sh:name "%A%"; + sh:targetClass ?class1; + sh:property [ sh:name ?data_point_name; + sh:path ?data_property; + sh:datatype ?datatype + ]. + FILTER (contains("%B%", ?data_point_name)) #The first value is from the data sigle point + } + + diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_object_property.rq b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_object_property.rq new file mode 100644 index 0000000..4222b3f --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/queries/triple_assembler_helper/vss/query_vss_object_property.rq @@ -0,0 +1,21 @@ +prefix pdmo: +prefix bmw: +prefix middleware: +prefix sh: +prefix val: +prefix xsd: +prefix ut: + + +SELECT ?class1 ?object_property ?class2 +WHERE { + ?S sh:name "%A%"; + sh:targetClass ?class1; + sh:property [ sh:name "%B%"; + sh:path ?object_property; + sh:class ?class2; + sh:nodeKind sh:IRI]. + + } + + diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/observation_shacl.ttl b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/observation_shacl.ttl new file mode 100644 index 0000000..3867fd0 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/observation_shacl.ttl @@ -0,0 +1,24 @@ +@prefix sh: . +@prefix sosa: . +@prefix xsd: . +@prefix val: . + +# Shape to validate an Observation +val:ObservationShape a sh:NodeShape ; + sh:targetClass sosa:Observation ; + sh:property [ + sh:path sosa:hasFeatureOfInterest ; + sh:nodeKind sh:IRI ; + ] ; + sh:property [ + sh:path sosa:observedProperty ; + sh:nodeKind sh:IRI ; + ] ; + sh:property [ + sh:path sosa:hasSimpleResult ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path sosa:phenomenonTime ; + sh:datatype xsd:dateTime ; + ] . diff --git a/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/vss_shacl.ttl b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/vss_shacl.ttl new file mode 100644 index 0000000..af446be --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/examples/use-case/model/shacl/vss_shacl.ttl @@ -0,0 +1,374 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . +@prefix bmw: . +@prefix val: . + + +val:VehicleShape a sh:NodeShape ; + sh:name "Vehicle" ; + sh:targetClass bmw:Vehicle ; + sh:property [ + sh:name "VIN" ; + sh:path bmw:vin ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "CurrentLocation" ; + sh:path bmw:hasSignal ; + sh:class bmw:CurrentLocation ; + sh:node val:CurrentLocationShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Speed" ; + sh:path bmw:speed ; + sh:datatype xsd:float ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "PWFStatus" ; + sh:path bmw:PWFStatus ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Cabin" ; + sh:path bmw:hasPart ; + sh:class bmw:Cabin ; + sh:node val:CabinShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Powertrain" ; + sh:path bmw:hasPart ; + sh:class bmw:Powertrain ; + sh:node val:PowertrainShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Chassis" ; + sh:path bmw:hasPart ; + sh:class bmw:Chassis ; + sh:node val:ChassisShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:ChassisShape a sh:NodeShape; + sh:name "Chassis"; + sh:targetClass bmw:Chassis; + sh:property [ + sh:name "SteeringWheel" ; + sh:path bmw:hasPart ; + sh:class bmw:SteeringWheel ; + sh:node val:SteeringWheelShape ; + sh:nodeKind sh:IRI ; + sh:minCount 1 ; + ] . + +val:SteeringWheelShape a sh:NodeShape; + sh:name "SteeringWheel"; + sh:targetClass bmw:SteeringWheel; + sh:property [ + sh:name "Angle" ; + sh:path bmw:angle ; + sh:datatype xsd:int; + sh:minCount 1 ; + ] . + + +val:CurrentLocationShape a sh:NodeShape ; + sh:name "CurrentLocation" ; + sh:targetClass bmw:CurrentLocation ; + sh:property [ + sh:name "Latitude" ; + sh:path bmw:latitude ; + sh:datatype xsd:double ; + sh:minInclusive -90 ; + sh:maxInclusive 90 ; + val:unit "degrees" ; + sh:description "Current latitude of vehicle in WGS 84 geodetic coordinates." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Longitude" ; + sh:path bmw:longitude ; + sh:datatype xsd:double ; + sh:minInclusive -180 ; + sh:maxInclusive 180 ; + val:unit "degrees" ; + sh:description "Current longitude of vehicle in WGS 84 geodetic coordinates." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Timestamp" ; + sh:path bmw:timestamp ; + sh:datatype xsd:long ; + val:unit "iso8601" ; + sh:description "Timestamp from GNSS system for current location, formatted according to ISO 8601 with UTC time zone." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Heading" ; + sh:path bmw:heading ; + sh:datatype xsd:double ; + sh:minInclusive 0 ; + sh:maxInclusive 360 ; + val:unit "degrees" ; + sh:description "Current heading relative to geographic north. 0 = North, 90 = East, 180 = South, 270 = West." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:PowertrainShape a sh:NodeShape; + sh:name "Powertrain"; + sh:targetClass bmw:Powertrain; + sh:property [ + sh:name "TractionBattery" ; + sh:path bmw:hasPart ; + sh:class bmw:TractionBattery ; + sh:node val:TractionBatteryShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Transmission" ; + sh:path bmw:hasPart ; + sh:class bmw:Transmission ; + sh:node val:TransmissionShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:TractionBatteryShape a sh:NodeShape; + sh:name "TractionBattery"; + sh:targetClass bmw:TractionBattery ; + sh:property [ + sh:name "NominalVoltage" ; + sh:path bmw:nominalVoltage ; + sh:datatype xsd:int ; + sh:description "Nominal voltage typically refers to voltage of fully charged battery when delivering rated capacity." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "StateOfCharge" ; + sh:path bmw:hasSignal ; + sh:class bmw:StateOfCharge; + sh:node val:StateOfChargeNodeShape; + sh:nodeKind sh:IRI ; + sh:description "Information on the state of charge of the vehicle's high voltage battery." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:StateOfChargeNodeShape a sh:NodeShape ; + sh:name "StateOfCharge" ; + sh:targetClass bmw:StateOfCharge ; + sh:property [ + sh:name "CurrentEnergy" ; + sh:path bmw:currentEnergy ; + sh:datatype xsd:float ; + sh:description "Physical state of charge of high voltage battery expressed in kWh." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + + +val:TransmissionShape a sh:NodeShape ; + sh:name "Transmission" ; + sh:targetClass bmw:Transmission ; + sh:property [ + sh:name "CurrentGear" ; + sh:path bmw:currentGear ; + sh:datatype xsd:int ; + sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + + +val:CabinShape a sh:NodeShape ; + sh:name "Cabin" ; + sh:targetClass bmw:Cabin ; + sh:property [ + sh:name "Light" ; + sh:path bmw:hasPart ; + sh:class bmw:CabinLight ; + sh:node val:CabinLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Door" ; + sh:path bmw:hasPart ; + sh:class bmw:CabinDoor ; + sh:node val:CabinDoorShape ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:CabinDoorShape a sh:NodeShape ; + sh:name "Door" ; + sh:targetClass bmw:CabinDoor ; + sh:property [ + sh:path bmw:position ; + sh:name "Row" ; + sh:in ("Row1" "Row2") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Driver" ; + sh:in ("Driver") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Passenger" ; + sh:path bmw:position ; + sh:in ("Passenger") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Window" ; + sh:path bmw:hasPart ; + sh:class bmw:Window ; + sh:node val:WindowShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:WindowShape a sh:NodeShape ; + sh:name "Window" ; + sh:targetClass bmw:Window ; + sh:property [ + sh:name "IsOpen" ; + sh:path bmw:isOpen ; + sh:datatype xsd:boolean ; + sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Position" ; + sh:path bmw:windowPosition ; + sh:datatype xsd:int ; + sh:minInclusive 0 ; + sh:maxInclusive 100 ; + val:unit "percent" ; + sh:description "Item position. 0 = Start position 100 = End position." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Switch" ; + sh:path bmw:switch ; + sh:datatype xsd:string ; + sh:in ("INACTIVE" "CLOSE" "OPEN" "ONE_SHOT_CLOSE" "ONE_SHOT_OPEN") ; + sh:description "Switch controlling sliding action such as window, sunroof, or blind." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:CabinLightShape a sh:NodeShape ; + sh:name "Light" ; + sh:targetClass bmw:CabinLight ; + sh:property [ + sh:name "AmbientLight" ; + sh:path bmw:hasPart ; + sh:class bmw:AmbientLight ; + sh:node val:AmbientLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "SpotLight" ; + sh:path bmw:hasPart ; + sh:class bmw:SpotLight ; + sh:node val:SpotLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:AmbientLightShape a sh:NodeShape ; + sh:name "AmbientLight" ; + sh:targetClass bmw:AmbientLight ; + sh:property [ + sh:path bmw:position ; + sh:name "Row" ; + sh:in ("Row1" "Row2") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Driver" ; + sh:in ("Driver") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Passenger" ; + sh:in ("Passenger") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Colour" ; + sh:path bmw:lightColor ; + sh:datatype xsd:string ; + sh:description "Hexadecimal color code represented as a 3-byte RGB (i.e. Red, Green, and Blue) value preceded by a hash symbol '#'."; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Intensity" ; + sh:path bmw:lightIntensity ; + sh:datatype xsd:integer ; + sh:minInclusive 1 ; + sh:maxInclusive 100 ; + val:unit "percent" ; + sh:description "How much of the maximum possible brightness of the light is used. 1 = Maximum attenuation, 100 = No attenuation (i.e. full brightness)." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "IsLightOn" ; + sh:path bmw:lightIsLightOn ; + sh:datatype xsd:boolean ; + sh:description "Indicates whether the light is turned on. True = On, False = Off." ; + sh:minCount 0 ; + sh:maxCount 1 ; +] . \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/pellet/README.md b/cdsp/knowledge-layer/symbolic-reasoner/pellet/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/pellet/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-adapter/README.md b/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-adapter/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-adapter/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-lib/README.md b/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-lib/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/pellet/pellet-lib/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md new file mode 100644 index 0000000..5d8d99d --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md @@ -0,0 +1,37 @@ + +# RDFox Integration + +This folder contains the necessary files to interact with RDFox, a high-performance knowledge graph and reasoning engine used in this project. + +## How to Use RDFox + +For instructions on how to start the RDFox service required for this project, see [this guide](../../../../docker/README.md#rdfox-restful-api). + +### RDFox Adapter + +See how to interact with the RDFox server using the [RDFox adapter](./src/README.md#rdfoxadapter). + +### Getting Started + +This project includes a small C++ application to verify that the RDFox service has been configured and started correctly. After compiling the project, you should be able to run the application from [`./rdfox-service-test/rdfox_test_main.cpp`](./rdfox-service-test/rdfox_test_main.cpp). The RDFox Test executable will be generated in the `/cdsp/knowledge-layer/build/bin/` directory. You can run it with the following command: + +```bash +$ ./websocket_client + +# Data store list: +# ?Name ?UniqueID ?Persistent ?Online ?Parameters + +# Data store 'family' does not exist. +# Creating a new data store 'family'. +# Data store created. +# Facts added. +# SPARQL query result: +# ?p ?n +# "Peter" +# "Stewie" +# "Chris" +# "Meg" +# "Lois" + +# Data store 'family' deleted successfully. +``` \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-lib/README.md b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-lib/README.md new file mode 100644 index 0000000..fd9e641 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-lib/README.md @@ -0,0 +1 @@ +tbd \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/CMakeLists.txt b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/CMakeLists.txt new file mode 100644 index 0000000..2f60cab --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/CMakeLists.txt @@ -0,0 +1,10 @@ +# Define the RDFox test target +add_executable( + rdfox_test rdfox_test_main.cpp +) + +# Link Boost libraries +target_link_libraries(rdfox_test Boost::system Boost::filesystem Boost::thread) + +# Use defined output directory for the executable +set_target_properties(rdfox_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/rdfox_test_main.cpp b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/rdfox_test_main.cpp new file mode 100644 index 0000000..d452743 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/rdfox-service-test/rdfox_test_main.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +const std::string RDFOX_SERVER = "localhost"; +const std::string PORT = "12110"; +const std::string AUTH_HEADER = + "Basic cm9vdDphZG1pbg=="; // Example for 'root:admin' encoded in base64 + +// Helper function to handle response status codes +void assert_response_ok(const http::response& res, const std::string& message) { + if (res.result() != http::status::ok && res.result() != http::status::created && + res.result() != http::status::no_content) { + std::cerr << message << "\nStatus: " << res.result_int() << "\n" << res.body() << std::endl; + throw std::runtime_error("Request failed."); + } +} + +// Function to perform a POST request +http::response post_request(tcp::socket& socket, beast::flat_buffer& buffer, + const std::string& target, const std::string& body, + const std::string& content_type) { + try { + http::request req{http::verb::post, target, 11}; + req.set(http::field::host, RDFOX_SERVER); + req.set(http::field::authorization, AUTH_HEADER); + req.set(http::field::content_type, content_type); + req.set(http::field::connection, "keep-alive"); // Keep connection alive + req.body() = body; + req.prepare_payload(); + + http::write(socket, req); + http::response res; + http::read(socket, buffer, res); + + return res; + } catch (const std::exception& e) { + std::cerr << "Error in POST request: " << e.what() << std::endl; + throw; + } +} + +// Function to perform a GET request +http::response get_request(tcp::socket& socket, beast::flat_buffer& buffer, + const std::string& target, + const std::string& accept_type) { + try { + http::request req{http::verb::get, target, 11}; + req.set(http::field::host, RDFOX_SERVER); + req.set(http::field::authorization, AUTH_HEADER); + req.set(http::field::accept, accept_type); + req.set(http::field::connection, "keep-alive"); // Keep connection alive + + http::write(socket, req); + http::response res; + http::read(socket, buffer, res); + + return res; + } catch (const std::exception& e) { + std::cerr << "Error in GET request: " << e.what() << std::endl; + throw; + } +} + +// Function to perform a DELETE request +http::response delete_request(tcp::socket& socket, beast::flat_buffer& buffer, + const std::string& target) { + try { + http::request req{http::verb::delete_, target, 11}; + req.set(http::field::host, RDFOX_SERVER); + req.set(http::field::authorization, AUTH_HEADER); + req.set(http::field::connection, "keep-alive"); // Keep connection alive + + http::write(socket, req); + http::response res; + http::read(socket, buffer, res); + + return res; + } catch (const std::exception& e) { + std::cerr << "Error in DELETE request: " << e.what() << std::endl; + throw; + } +} + +// Main function to use persistent connections +int main() { + try { + net::io_context ioc; + tcp::resolver resolver(ioc); + tcp::socket socket(ioc); + auto const results = resolver.resolve(RDFOX_SERVER, PORT); + net::connect(socket, results.begin(), results.end()); + beast::flat_buffer buffer; + + std::string store_name = "family"; + + // Step 1: Check if the 'family' data store exists and delete it if necessary + auto res = get_request(socket, buffer, "/datastores", "application/json"); + assert_response_ok(res, "Failed to obtain list of datastores."); + std::cout << "Data store list:\n" << res.body() << std::endl; + + if (res.body().find(store_name) != std::string::npos) { + std::cout << "Data store '" << store_name << "' exists. Deleting..." << std::endl; + res = delete_request(socket, buffer, "/datastores/" + store_name); + assert_response_ok(res, "Failed to delete data store."); + std::cout << "Data store '" << store_name << "' deleted successfully." << std::endl; + } else { + std::cout << "Data store '" << store_name << "' does not exist." << std::endl; + } + + // Step 2: Create a new data store + std::cout << "Creating a new data store '" << store_name << "'." << std::endl; + res = post_request(socket, buffer, "/datastores/family?type=parallel-ww", "", + "application/json"); + assert_response_ok(res, "Failed to create datastore."); + std::cout << "Data store created." << std::endl; + + // Step 3: Add facts in Turtle format + std::string turtle_data = R"( + @prefix : . + :peter :forename "Peter" ; + a :Person ; + :marriedTo :lois ; + :gender "male" . + :lois :forename "Lois" ; + a :Person ; + :gender "female" . + :meg :forename "Meg" ; + a :Person ; + :hasParent :lois, :peter ; + :gender "female" . + :chris :forename "Chris" ; + a :Person ; + :hasParent :peter ; + :gender "male" . + :stewie :forename "Stewie" ; + a :Person ; + :hasParent :lois ; + :gender "male" . + :brian :forename "Brian" . + )"; + res = + post_request(socket, buffer, "/datastores/family/content", turtle_data, "text/turtle"); + assert_response_ok(res, "Failed to add facts to data store."); + std::cout << "Facts added." << std::endl; + + // Step 4: Run a SPARQL query + std::string sparql_query = R"( + PREFIX : + SELECT ?p ?n WHERE { ?p a :Person . ?p :forename ?n } + )"; + res = post_request(socket, buffer, "/datastores/family/sparql", sparql_query, + "application/sparql-query"); + assert_response_ok(res, "Failed to run SPARQL query."); + std::cout << "SPARQL query result:\n" << res.body() << std::endl; + + res = delete_request(socket, buffer, "/datastores/" + store_name); + assert_response_ok(res, "Failed to delete data store."); + std::cout << "Data store '" << store_name << "' deleted successfully." << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + return 0; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/README.md b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/README.md new file mode 100644 index 0000000..099f931 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/README.md @@ -0,0 +1,25 @@ +# RDFoxAdapter + +The RDFoxAdapter class provides an interface for interacting with an RDFox server over HTTP. It enables operations like checking the existence of a datastore, creating a datastore, loading data in Turtle format, querying using SPARQL, and deleting a datastore. + +> [!NOTE] Data store +> When the RDFoxAdapter initializes creates (if is does not exists) a datastore called `vehicle_ds` in the RDFox server. + +## Features + +- **Initialize a Datastore:** Checks if a specified datastore exists on the RDFox server. If not, it creates the datastore. +- **Load Data:** Loads Turtle data into a specified datastore. +- **Query Data:** Executes SPARQL queries against the RDFox datastore and retrieves results. +- **Delete Datastore:** Removes a specified datastore from the RDFox server. +- **Send HTTP Requests:** Supports sending GET and POST HTTP requests to interact with RDFox. + +## Methods + +- **initialize():** Ensures the datastore is present; creates it if missing. +- **loadData(const std::string& ttl_data):** Loads Turtle data into the datastore. +- **queryData(const std::string& sparql_query):** Executes a SPARQL query and returns the result. +- **deleteDataStore():** Deletes the datastore if it exists. + +> [!NOTE] See test +> - [../tests/test_rdfox_adapter_integration.cpp](../tests/test_rdfox_adapter_integration.cpp) +> - [../tests/test_rdfox_adapter_unit.cpp](../tests/test_rdfox_adapter_unit.cpp) \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.cpp b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.cpp new file mode 100644 index 0000000..cdf2622 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.cpp @@ -0,0 +1,179 @@ +#include "rdfox-adapter.h" + +#include + +RDFoxAdapter::RDFoxAdapter(const std::string& host, const std::string& port, + const std::string& auth_base64, + const std::string& data_store = "vehicle_ds") + : host_(host), + port_(port), + auth_header_base64_("Basic " + auth_base64), + data_store_(data_store) { + std::cout << "Initializing RDFox adapter..." << std::endl; +} + +/** + * @brief Initializes the RDFoxAdapter by ensuring the data store is created. + * + * This method checks if the data store specified by `data_store_` exists. + * If the data store exists, it logs a message indicating its existence. + * If the data store does not exist, it attempts to create it by sending + * a POST request to the appropriate endpoint. If the creation is successful, + * a success message is logged. Otherwise, an exception is thrown. + * + * @throws std::runtime_error if the data store creation fails. + */ +void RDFoxAdapter::initialize() { + // checks if the data store exists, create it if not + if (checkDataStore()) { + std::cout << "Data store '" + data_store_ + "' is already created." << std::endl; + } else { + std::cout << "Data store '" << data_store_ << "' does not exist. Creating it..." + << std::endl; + // Creates a data store + std::string target = "/datastores/" + data_store_; + if (sendPostRequest(target, "", "application/json")) { + std::cout << "Data store '" << data_store_ << "' created successfully." << std::endl; + } else { + throw std::runtime_error("Failed to create datastore '" + data_store_ + "'"); + } + } +} + +/** + * Loads Turtle data into the RDFox datastore. + * + * @param ttl_data A string containing the Turtle data to be loaded into the datastore. + * @return A boolean value indicating whether the data was successfully loaded. + */ +bool RDFoxAdapter::loadData(const std::string& ttl_data) { + std::string target = "/datastores/" + data_store_ + "/content"; + return sendPostRequest(target, ttl_data, "text/turtle") ? true : false; +}; + +/** + * Executes a SPARQL query against the RDFox datastore. + * + * @param sparql_query The SPARQL query string to be executed. + * @return The response from the datastore as a string if the query is successful, + * otherwise an empty string. + */ +std::string RDFoxAdapter::queryData(const std::string& sparql_query) { + std::string target = "/datastores/" + data_store_ + "/sparql"; + auto response = sendPostRequest(target, sparql_query, "application/sparql-query"); + return response.has_value() ? response.value() : ""; +} + +bool RDFoxAdapter::deleteDataStore() { + if (checkDataStore()) { + std::string target = "/datastores/" + data_store_; + std::string responseBody; + if (sendRequest(http::verb::delete_, target, "", "", "", responseBody)) { + std::cout << "Data store '" + data_store_ + "' have been removed successfully." + << std::endl; + } else { + std::cout << "Data store '" + data_store_ + "' could not be removed." << std::endl; + return false; + } + } else { + std::cout << "Data store '" + data_store_ + "' does not exists anymore." << std::endl; + } + return true; +} + +/** + * @brief Checks if the data store exists on the server. + * + * @return true if the data store is found in the server's response; false otherwise. + */ +bool RDFoxAdapter::checkDataStore() { + std::string target = "/datastores"; + std::string response = sendGetRequest(target, "text/csv; charset=UTF-8"); + if (response.find(data_store_) != std::string::npos) { + return true; + } else { + return false; + } +} + +std::optional RDFoxAdapter::sendPostRequest(const std::string& target, + const std::string& body, + const std::string& contentType) { + std::string responseBody; + if (sendRequest(http::verb::post, target, body, contentType, "", responseBody)) { + return responseBody; + } + return std::nullopt; +} + +std::string RDFoxAdapter::sendGetRequest(const std::string& target, const std::string& acceptType) { + std::string responseBody; + if (sendRequest(http::verb::get, target, "", "", acceptType, responseBody)) { + return responseBody; + } + return ""; +} + +/** + * Sends an HTTP request to a specified target and retrieves the response. + * + * @param method The HTTP method to use for the request (e.g., GET, POST). + * @param target The target URI for the request. + * @param body The body content to send with the request. + * @param contentType The MIME type of the body content. + * @param acceptType The MIME type that the client is willing to accept in the response. + * @param responseBody A reference to a string where the response body will be stored. + * @return True if the request was successful and the response status is OK, Created, or No Content; + * false otherwise. + */ +bool RDFoxAdapter::sendRequest(http::verb method, const std::string& target, + const std::string& body, const std::string& contentType, + const std::string& acceptType, std::string& responseBody) { + net::io_context ioc; + tcp::resolver resolver(ioc); + tcp::socket socket(ioc); + + try { + // Resolve and connect to the host + auto const results = resolver.resolve(host_, port_); + net::connect(socket, results.begin(), results.end()); + + // Set up the HTTP request + http::request req{method, target, 11}; + req.set(http::field::host, host_); + req.set(http::field::authorization, auth_header_base64_); + if (!contentType.empty()) { + req.set(http::field::content_type, contentType); + } + if (!acceptType.empty()) { + req.set(http::field::accept, acceptType); + } + req.body() = body; + req.prepare_payload(); + + // Send the request and receive the response + http::write(socket, req); + beast::flat_buffer buffer; + http::response res; + http::read(socket, buffer, res); + + responseBody = res.body(); + + if (res.result() != http::status::ok && res.result() != http::status::created && + res.result() != http::status::no_content) { + std::cerr << createErrorMessage(res.body(), res.result_int()) << std::endl; + return false; + } + return true; + } catch (const beast::system_error& e) { + std::cerr << "Network error: " << e.what() << std::endl; + return false; + } catch (const std::exception& e) { + std::cerr << "Error in request: " << e.what() << std::endl; + return false; + } +} + +std::string RDFoxAdapter::createErrorMessage(const std::string& error_msg, int error_code) { + return error_msg + " (Code: " + std::to_string(error_code) + ")"; +} diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.h b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.h new file mode 100644 index 0000000..66bcfb1 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/src/rdfox-adapter.h @@ -0,0 +1,42 @@ +#ifndef RDFOX_ADAPTER_H +#define RDFOX_ADAPTER_H + +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +class RDFoxAdapter { + public: + RDFoxAdapter(const std::string& host, const std::string& port, const std::string& auth_base64, + const std::string& data_store); + + void initialize(); + bool loadData(const std::string& ttl_data); + std::string queryData(const std::string& sparql_query); + bool checkDataStore(); + bool deleteDataStore(); + + protected: + virtual bool sendRequest(http::verb method, const std::string& target, const std::string& body, + const std::string& contentType, const std::string& acceptType, + std::string& responseBody); + + private: + std::string sendGetRequest(const std::string& target, const std::string& accept_type); + std::optional sendPostRequest(const std::string& target, const std::string& body, + const std::string& content_type); + + std::string createErrorMessage(const std::string& error_msg, int error_code); + std::string host_; + std::string port_; + std::string auth_header_base64_; + std::string data_store_; +}; + +#endif // RDFOX_ADAPTER_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/CMakeLists.txt b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/CMakeLists.txt new file mode 100644 index 0000000..f52b87c --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/CMakeLists.txt @@ -0,0 +1,38 @@ +# GoogleTest integration +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 +) +# Disable installing gtest (we don't need to install it) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Add the RDFox adapter source files +add_library(rdfox_adapter STATIC ../src/rdfox-adapter.cpp) +target_include_directories(rdfox_adapter PUBLIC ../src) +target_link_libraries(rdfox_adapter Boost::system Boost::filesystem Boost::thread nlohmann_json) + +# Add the unit test executable +add_executable(rdfox_unit_tests test_rdfox_adapter_unit.cpp) +target_include_directories(rdfox_unit_tests PUBLIC ../src) +target_link_libraries(rdfox_unit_tests GTest::gtest GTest::gtest_main GTest::gmock rdfox_adapter) + +# Add the integration test executable +add_executable(rdfox_integration_tests test_rdfox_adapter_integration.cpp) +target_include_directories(rdfox_integration_tests PUBLIC ../src) +target_link_libraries(rdfox_integration_tests GTest::gtest GTest::gtest_main rdfox_adapter) + +# Add unit and integration tests to CTest +add_test(NAME RDFoxAdapterUnitTests COMMAND rdfox_unit_tests) +add_test(NAME RDFoxAdapterIntegrationTests COMMAND rdfox_integration_tests) + +# Define custom output directory for test binaries +set_target_properties(rdfox_unit_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set_target_properties(rdfox_integration_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +# Ensure tests are built with the all target +add_custom_target(tests ALL + DEPENDS rdfox_unit_tests rdfox_integration_tests +) \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_integration.cpp b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_integration.cpp new file mode 100644 index 0000000..8cb8a74 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_integration.cpp @@ -0,0 +1,78 @@ +#include + +#include + +#include "../src/rdfox-adapter.h" + +class RDFoxAdapterIntegrationTest : public ::testing::Test { + protected: + RDFoxAdapter* adapter; + + std::string host = "localhost"; + std::string port = "12110"; + std::string auth_base64 = "cm9vdDphZG1pbg=="; // Base64 encoded authorization string + std::string data_store = "test_ds"; + + void SetUp() override { + adapter = new RDFoxAdapter(host, port, auth_base64, data_store); + adapter->initialize(); + + // Initial load to ensure datastore is accessible + if (!adapter->checkDataStore()) { + FAIL() << "Failed to ensure the data store is set up."; + } + } + + void TearDown() override { + // Delete the test data store after all tests have completed + // ASSERT_TRUE(adapter->deleteDataStore()) << "Failed to clean up the test data store."; + adapter->deleteDataStore(); + + // Clean up the adapter instance + delete adapter; + } +}; + +// Integration Test: Ensure RDFoxAdapter can create a data store and retrieve it +TEST_F(RDFoxAdapterIntegrationTest, DataStoreInitializationTest) { + // This test is implicitly done during the setup phase of RDFoxAdapter + // If the data store exists or is created, no error will be thrown + // You can further assert behavior here if needed + SUCCEED(); +} + +TEST_F(RDFoxAdapterIntegrationTest, LoadDataTest) { + std::string ttlData = R"( + @prefix bmw: . + bmw:Vehicle1 a bmw:Vehicle . + )"; + + // Try loading data into the data store + ASSERT_TRUE(adapter->loadData(ttlData)) << "Failed to load Turtle data into the store"; +} + +TEST_F(RDFoxAdapterIntegrationTest, QueryDataTest) { + std::string ttlData = R"( + @prefix bmw: . + bmw:Vehicle1 a bmw:Vehicle . + )"; + ASSERT_TRUE(adapter->loadData(ttlData)) << "Failed to load Turtle data into the store"; + + std::string sparqlQuery = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"; + std::string result = adapter->queryData(sparqlQuery); + + ASSERT_FALSE(result.empty()) << "SPARQL query returned no results"; + EXPECT_TRUE(result.find("") != std::string::npos) + << "Result does not contain the expected subject."; + EXPECT_TRUE(result.find("") != + std::string::npos) + << "Result does not contain the expected predicate."; + EXPECT_TRUE(result.find("") != std::string::npos) + << "Result does not contain the expected object."; +} + +TEST_F(RDFoxAdapterIntegrationTest, DeleteDataStoreTest) { + ASSERT_TRUE(adapter->checkDataStore()) << "Any data store has been created."; + ASSERT_TRUE(adapter->deleteDataStore()) << "Failed to delete the data store."; + ASSERT_FALSE(adapter->checkDataStore()) << "The datastore might still exist or be recreated."; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_unit.cpp b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_unit.cpp new file mode 100644 index 0000000..7834418 --- /dev/null +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/test_rdfox_adapter_unit.cpp @@ -0,0 +1,193 @@ +#include +#include + +#include "../src/rdfox-adapter.h" + +class MockRDFoxAdapter : public RDFoxAdapter { + public: + MockRDFoxAdapter() : RDFoxAdapter("localhost", "8080", "test_auth", "test_ds") {} + + MOCK_METHOD6(sendRequest, bool(http::verb, const std::string&, const std::string&, + const std::string&, const std::string&, std::string&)); +}; + +// Test checking nonexistent RDFox datastore. +TEST(RDFoxAdapterTest, CheckNonexistentDatastore) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking nonexistent datastore. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>(""), testing::Return(false))); + + EXPECT_FALSE(mock_adapter.checkDataStore()); +} + +// Test checking existent RDFox datastore. +TEST(RDFoxAdapterTest, CheckExistentDatastore) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking existent datastore. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>("test_ds"), testing::Return(true))); + + EXPECT_TRUE(mock_adapter.checkDataStore()); +} + +// Test the initialization of the RDFoxAdapter and creation of the datastore. +TEST(RDFoxAdapterTest, InitializationCreatesDatastore) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking datastore does not exists. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>(""), testing::Return(false))); + + // Mock the sendRequest for creating the datastore. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds", "", + "application/json", "", testing::_)) + .WillOnce(testing::Return(true)); + + EXPECT_NO_THROW(mock_adapter.initialize()); +} + +// Test the initialization of the RDFoxAdapter when datastore already exists. +TEST(RDFoxAdapterTest, InitializationWithExistingDatastore) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking datastore exists. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>("test_ds"), testing::Return(true))); + + // Ensure that the second call for creating the datastore is not made. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds", "", + "application/json", "", testing::_)) + .Times(0); + + EXPECT_NO_THROW(mock_adapter.initialize()); +} + +// Test loading data into the RDFox datastore. +TEST(RDFoxAdapterTest, LoadDataSuccess) { + MockRDFoxAdapter mock_adapter; + std::string ttl_data = "@prefix : . :test a :Entity ."; + + // Mock the sendRequest for loading data. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds/content", ttl_data, + "text/turtle", "", testing::_)) + .WillOnce(testing::Return(true)); + + EXPECT_TRUE(mock_adapter.loadData(ttl_data)); +} + +// Test querying data from the RDFox datastore. +TEST(RDFoxAdapterTest, QueryDataSuccess) { + MockRDFoxAdapter mock_adapter; + std::string sparql_query = "SELECT ?s WHERE { ?s ?p ?o . }"; + std::string mock_response = ""; + + // Mock the sendRequest for querying data. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds/sparql", + sparql_query, "application/sparql-query", "", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>(mock_response), testing::Return(true))); + + EXPECT_EQ(mock_adapter.queryData(sparql_query), mock_response); +} + +// Test deleting the RDFox datastore. +TEST(RDFoxAdapterTest, DeleteDataStoreSuccess) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking the datastore existence. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>("test_ds"), testing::Return(true))); + + // Mock the sendRequest for deleting the datastore. + EXPECT_CALL(mock_adapter, + sendRequest(http::verb::delete_, "/datastores/test_ds", "", "", "", testing::_)) + .WillOnce(testing::Return(true)); + + EXPECT_TRUE(mock_adapter.deleteDataStore()); +} + +// Test any error deleting the RDFox datastore that does not exists. +TEST(RDFoxAdapterTest, DeleteNonexistentDataStoreSuccess) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking the datastore existence. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>(""), testing::Return(false))); + + // Ensure that the second call for deleting the datastore is not made. + EXPECT_CALL(mock_adapter, + sendRequest(http::verb::delete_, "/datastores/test_ds", "", "", "", testing::_)) + .Times(0); + + EXPECT_TRUE(mock_adapter.deleteDataStore()); +} + +// Test handling a failed request during datastore creation. +TEST(RDFoxAdapterTest, FailedToCreateDataStore) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking the datastore existence. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>(""), testing::Return(true))); + + // Mock the sendRequest for creating the datastore failure. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds", "", + "application/json", "", testing::_)) + .WillOnce(testing::Return(false)); + + // Expect a runtime error when the adapter tries to create the datastore. + EXPECT_THROW(mock_adapter.initialize(), std::runtime_error); +} + +// Test loading data when the request fails. +TEST(RDFoxAdapterTest, LoadDataFailure) { + MockRDFoxAdapter mock_adapter; + std::string ttl_data = "@prefix : . :test a :Entity ."; + + // Mock the sendRequest for loading data failure. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds/content", ttl_data, + "text/turtle", "", testing::_)) + .WillOnce(testing::Return(false)); + + EXPECT_FALSE(mock_adapter.loadData(ttl_data)); +} + +// Test querying data when the request fails. +TEST(RDFoxAdapterTest, QueryDataFailure) { + MockRDFoxAdapter mock_adapter; + std::string sparql_query = "SELECT ?s WHERE { ?s ?p ?o . }"; + + // Mock the sendRequest for querying data failure. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::post, "/datastores/test_ds/sparql", + sparql_query, "application/sparql-query", "", testing::_)) + .WillOnce(testing::Return(false)); + + // Call queryData and expect it to return an empty string. + EXPECT_EQ(mock_adapter.queryData(sparql_query), ""); +} + +// Test deleting the RDFox datastore when delete request fails. +TEST(RDFoxAdapterTest, DeleteDataStoreFailure) { + MockRDFoxAdapter mock_adapter; + + // Mock the sendRequest for checking the datastore existence. + EXPECT_CALL(mock_adapter, sendRequest(http::verb::get, "/datastores", "", "", + "text/csv; charset=UTF-8", testing::_)) + .WillOnce(testing::DoAll(testing::SetArgReferee<5>("test_ds"), testing::Return(true))); + + // Mock the sendRequest for deleting the datastore failure. + EXPECT_CALL(mock_adapter, + sendRequest(http::verb::delete_, "/datastores/test_ds", "", "", "", testing::_)) + .WillOnce(testing::Return(false)); + + EXPECT_FALSE(mock_adapter.deleteDataStore()); +} \ No newline at end of file diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..ed6d4c4 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,8 @@ +# Docker generated files +data/ +logs/ +test-model/ +rdfox/ + +# Enviroment variables containing secrets +.env \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 1913f13..ee1d23c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -3,6 +3,13 @@ This directory contains files related to the Docker deployment of the Central Da # Central Data Service Playground The compose file `docker-compose-cdsp.yml` provides a containerized deployment of the playground using docker. +## Table of contents +- [Docker installation](#docker-installation) +- [VISSR docker image build setup](#vissr-docker-image-build-setup) +- [Websocket-Server (CDSP - information layer) docker image build setup](#websocket-server-cdsp---information-layer-docker-image-build-setup) +- [Websocket-Client (CDSP - knowledge layer) docker image build setup](#websocket-client-cdsp---knowledge-layer-docker-image-build-setup) +- [Deploy with Docker Compose](#deploy-with-docker-compose) + ## Docker installation If you are not familiar with Docker it is a widely deployed and well documented technology. For which you should find numerous tutorials on its use on the internet. @@ -32,7 +39,7 @@ Depending on your distro you may need to setup `GOROOT` and `GOPATH`. This was n ### Create persistent local volume `/tmp/docker` The upstream Docker compose assumes the existence of the local directory `/tmp/docker` but does not create it. Fix the issue by creating it yourself: -``` +```shell $ mkdir /tmp/docker ``` See upstream issue report https://github.com/w3c/automotive-viss2/issues/99 for details. @@ -44,8 +51,8 @@ See upstream issue report https://github.com/w3c/automotive-viss2/issues/86 for ### Disable Access Grant support (agt_public_key.rsa build error) There is a current issue with the upstream VISSR VISS Server Dockerfile in which the server fails to build due to a missing public key for the Access Control server. See upstream issue report https://github.com/w3c/automotive-viss2/issues/88 for details. After discussion with the upstream maintainers the current workaround is to comment out the relevant following line from the end of the `vissv2server` section of the Dockerfile. The change should be made to `cdsp/cdsp/vissr/Dockerfile.rlserver` -``` -#COPY --from=builder /build/server/agt_server/agt_public_key.rsa . +```shell +# COPY --from=builder /build/server/agt_server/agt_public_key.rsa . ``` If your project requires Access Grant support please discuss enabling it with the VISSR community. @@ -72,8 +79,8 @@ A solution is to add the CA root certificate to the VISSR Dockerfile which will The VISSR Dockerfile already includes an example of this for a Cisco system which uses the CA root certificate `cisco.crt`. From `cdsp/vissr/Dockerfile.rlserver`: -``` -#corporate proxy settings can sometimes cause tls verification error. Add root crt to docker container. +```dockerfile +# corporate proxy settings can sometimes cause tls verification error. Add root crt to docker container. COPY testCredGen/cicso-umbrella/cisco.crt /usr/local/share/ca-certificates/cisco.crt RUN update-ca-certificates ``` @@ -82,7 +89,7 @@ To add your own: 2. Copy the `.crt` file into `cdsp/vissr/testCredGen` 3. Add your own line to the Dockerfile to copy the certificate so that it is included when `update-ca-certificates` is run. As shown below. -``` +```dockerfile COPY testCredGen/cicso-umbrella/cisco.crt /usr/local/share/ca-certificates/cisco.crt COPY testCredGen/ /usr/local/share/ RUN update-ca-certificates @@ -97,39 +104,142 @@ You can start working with the playground by starting the deployment with just t In parallel you can work through any VISSR build issues to add VISS support northbound and the other features it supports. Deploy (start) just IoTDB: +```shell +$ sudo docker compose -f docker-compose-cdsp.yml up -d iotdb-service +# [+] Running 1/0 +# ✔ Container iotdb-service Running 0.0s +``` + +## Websocket-Server (CDSP - information layer) docker image build setup +This guide provides instructions for deploying the Websocket-Server using Docker Compose. You can choose to deploy the server with either RealmDB or IoTDB as the backend service. + +### Prerequisites + +- Correct configuration of the `.env` file based on the database you are using (RealmDB or IoTDB) + +### Configuring the `.env` File + +The `.env` file contains environment variables that need to be configured depending on the database you are using. Detailed instructions on how to configure the `.env` file can be found in the [database handlers README](../cdsp/information-layer/handlers/README.md). + +> [!IMPORTANT] +> Before proceeding with the deployment, ensure that the `.env` file is correctly configured for the database you plan to use. + +### Using RealmDB + +To deploy the Websocket-Server with RealmDB, use the following command: + +```shell +$ sudo docker compose -f docker-compose-cdsp.yml up -d websocket-service +# [+] Running 1/1 +# ✔ Container websocket-service Started 0.4s ``` +### Using IoTDB + +When deploying with IoTDB, ensure that the iotdb-service is up and running before starting the Websocket-Server. + +> [!IMPORTANT] +> If required, ensure that the iotdb-service container is started before running the Websocket-Server. You can also configure your own IoTDB connection. + +#### Start IoTDB and Websocket-Server + +To start both the iotdb-service and websocket-service, use the following commands: + +```shell $ sudo docker compose -f docker-compose-cdsp.yml up -d iotdb-service -[+] Running 1/0 - ✔ Container iotdb-service Running 0.0s +# [+] Running 1/1 +# ✔ Container iotdb-service Started 0.4s 0.4s + +$ sudo docker compose -f docker-compose-cdsp.yml up -d websocket-service +# [+] Running 1/1 +# ✔ Container websocket-service Started 0.4s +``` + +### Expected result + +Listing should show three running containers as shown below: +```shell +$ sudo docker ps +``` + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +025b5dd05c56 cdsp-websocket-service "docker-entrypoint.s…" 16 minutes ago Up 16 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp websocket-service +e16c8ed4ed42 apache/iotdb:1.2.2-standalone "/usr/bin/dumb-init …" 23 minutes ago Up 19 minutes 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp iotdb-service +``` + +## Websocket client (CDSP - knowledge layer) docker image build setup + +### RDFox RESTful API + +In order to get access to `RDFox RESTfull API` it is required to build two Docker images (`rdfox-init` and `rdfox-service`). + +### Prerequisites + +- **Running Websocket-Server:** See how to start the information layer [here](#websocket-server-cdsp---information-layer-docker-image-build-setup). +- **RDFox.lic:** this is the license file required by RDFox, containing information that authorizes the use of RDFox, expiry time and usage. **The license file must be provided when running RDFox images to activate the software.**. The path of the file should be provided using the environment variable `RDFOX_LIC_PATH="/RDFox.lic"` in the `.env` file in this folder. The file is generally provided when you acquire a license from [Oxford Semantic Technologies](https://www.oxfordsemantic.tech/). + +#### 1. **Initialization (`rdfox-init` Image)**: +- The `rdfox-init` image is used to set up the RDFox server directory, define roles, and configure persistence. +- It only needs to be run **once** for a fresh setup or if you need to reset the server's configuration (e.g., to initialize roles, passwords, and persistence settings). +- Running this command creates a persistent volume (`rdfox-server-directory`) that stores RDFox's state, including the role configuration, data stores, and settings. + +> [!IMPORTANT] +> The image `rdfox-init` need to run only ones to initialize the RDFox server directory. + +#### 2. **Daemon (`rdfox-service` Image)**: +- The `rdfox-service` image runs the **RDFox server (daemon)**, which continuously serves requests on port 12110. +- Once initialized, you can start this container (as a daemon) and it will use the `rdfox-server-directory` volume created by the `rdfox-init` command. +- You can restart the daemon multiple times using the same persistent volume, and it will retain all previously initialized settings and data. +- The daemon is initialized with a default role `root` and password `admin`. + +> [!WARNING] +> Before build the `rdfox-service`, ensure that the `rdfox-init` were compiled correctly. Only if the logs show successful completion, you can then proceed to start the RDFox daemon. + +Use the following commands to start both images: + +```shell +$ docker compose -f docker-compose-cdsp.yml up -d rdfox-init +# ... +# [+] Running 2/2 +# ✔ Volume "cdsp_rdfox-server-directory" created 0.0s +# ✔ Container rdfox-init Started 0.4s + +$ docker compose -f docker-compose-cdsp.yml up -d rdfox-service +# ... +# [+] Running 2/2 +# ✔ Container rdfox-init Started 0.4s +# ✔ Container rdfox-service Started 0.9s ``` ## Deploy with Docker Compose ### Start/stop containers Start the containers: -``` +```shell $ sudo docker compose -f docker-compose-cdsp.yml up -d -[+] Running 4/5 - ⠴ Network cdsp_default Created 1.5s - ✔ Container vissr_container_volumes Started 0.5s - ✔ Container iotdb-service Started 0.7s - ✔ Container app_redis Started 1.0s - ✔ Container vissv2server Started 1.3s +# [+] Running 4/5 +# ⠴ Network cdsp_default Created 1.5s +# ✔ Container vissr_container_volumes Started 0.5s +# ✔ Container iotdb-service Started 0.7s +# ✔ Container app_redis Started 1.0s +# ✔ Container vissv2server Started 1.3s ``` Stop and remove the containers: -``` +```shell $ sudo docker compose -f docker-compose-cdsp.yml down -[+] Running 5/5 - ✔ Container vissv2server Removed 0.0s - ✔ Container app_redis Removed 0.3s - ✔ Container iotdb-service Removed 2.2s - ✔ Container vissr_container_volumes Removed 0.0s - ✔ Network cdsp_default Removed 0.1s +# [+] Running 5/5 +# ✔ Container vissv2server Removed 0.0s +# ✔ Container app_redis Removed 0.3s +# ✔ Container iotdb-service Removed 2.2s +# ✔ Container vissr_container_volumes Removed 0.0s +# ✔ Network cdsp_default Removed 0.1s ``` ### Expected Result Listing should show three running containers as shown below: -``` +```shell $ sudo docker ps +``` +``` NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS app_redis redis "docker-entrypoint.s…" redis 10 minutes ago Up 10 minutes 6379/tcp iotdb-service apache/iotdb:1.2.2-standalone "/usr/bin/dumb-init …" iotdb-service 10 minutes ago Up 10 minutes 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp @@ -137,8 +247,10 @@ vissv2server cdsp-vissv2server "/app/vissv2server -…" vissv ``` #### Apache IoTDB You can confirm the Apache IoTDB server is running by connecting to it with the IoTDB CLI client (_quit_ to exit the client): -``` +```shell $ sudo docker exec -ti iotdb-service /iotdb/sbin/start-cli.sh -h iotdb-service +``` +``` --------------------- Starting IoTDB Cli --------------------- diff --git a/docker/docker-compose-cdsp.yml b/docker/docker-compose-cdsp.yml index 8ef75a5..3796868 100644 --- a/docker/docker-compose-cdsp.yml +++ b/docker/docker-compose-cdsp.yml @@ -27,48 +27,102 @@ services: - dn_target_config_node_list=iotdb-service:10710 volumes: - ./data:/iotdb/data - - ./logs:/iotdb/logs + - ./logs/iotdb:/iotdb/logs -# VISSR acting as VISS Data Server - - tmp: - image: busybox - container_name: vissr_container_volumes - user: root - command: > - chmod -R 777 /tmp/docker/ +# WebSocket server service + websocket-service: + build: + context: ../cdsp/information-layer + dockerfile: Dockerfile + container_name: websocket-service + ports: + - "8080:8080" volumes: - - /tmp/docker/ + - ./logs/information-layer:/app/logs + env_file: + - .env - redis: - image: redis - container_name: app_redis - privileged: true - user: root - restart: on-failure:3 - command: redis-server /etc/redis.conf + # RDFox Initialization (run once) + rdfox-init: + image: oxfordsemantic/rdfox-init:latest + container_name: rdfox-init + platform: linux/amd64 volumes: - - ../cdsp/vissr/redis/redis.conf:/etc/redis.conf - volumes_from: - - tmp + - ${RDFOX_LIC_PATH}:/opt/RDFox/RDFox.lic # License file on host + - rdfox-server-directory:/home/rdfox/.RDFox + environment: + - RDFOX_ROLE=root + - RDFOX_PASSWORD=admin + command: | + -persistence file + restart: "no" - vissv2server: - container_name: vissv2server + # RDFox Daemon (persistent service) + rdfox-service: + image: oxfordsemantic/rdfox:latest + container_name: rdfox-service + platform: linux/amd64 depends_on: - - redis - - iotdb-service - restart: on-failure:3 - build: - context: ../cdsp/vissr # context set to repo root - dockerfile: Dockerfile.rlserver - target: vissv2server - entrypoint: [ /app/vissv2server,-s,apache-iotdb] + - rdfox-init ports: - - "0.0.0.0:8081:8081" - - "127.0.0.1:8888:8888" - - "0.0.0.0:8887:8887" - - "0.0.0.0:8600:8600" + - "12110:12110" volumes: - - ./logs:/app/logs - volumes_from: - - tmp + - ${RDFOX_LIC_PATH}:/opt/RDFox/RDFox.lic # License file on host + - rdfox-server-directory:/home/rdfox/.RDFox + environment: + - RDFOX_ROLE=root + - RDFOX_PASSWORD=admin + command: daemon + deploy: + resources: + limits: + memory: 8g + cpus: "2.0" + restart: "always" + + # VISSR acting as VISS Data Server + + tmp: + image: busybox + container_name: vissr_container_volumes + user: root + command: > + chmod -R 777 /tmp/docker/ + volumes: + - /tmp/docker/ + + redis: + image: redis + container_name: app_redis + privileged: true + user: root + restart: on-failure:3 + command: redis-server /etc/redis.conf + volumes: + - ../cdsp/vissr/redis/redis.conf:/etc/redis.conf + volumes_from: + - tmp + + vissv2server: + container_name: vissv2server + depends_on: + - redis + - iotdb-service + restart: on-failure:3 + build: + context: ../cdsp/vissr # context set to repo root + dockerfile: Dockerfile.rlserver + target: vissv2server + entrypoint: [ /app/vissv2server,-s,apache-iotdb] + ports: + - "0.0.0.0:8081:8081" + - "127.0.0.1:8888:8888" + - "0.0.0.0:8887:8887" + - "0.0.0.0:8600:8600" + volumes: + - ./logs/vssr:/app/logs + volumes_from: + - tmp + +volumes: + rdfox-server-directory: diff --git a/docker/start_docker.sh b/docker/start_docker.sh new file mode 100644 index 0000000..e69de29 diff --git a/examples/knowledgelayer-hello-world/KL-example-readme-graphic.png b/examples/knowledgelayer-hello-world/KL-example-readme-graphic.png new file mode 100644 index 0000000..3222fe0 Binary files /dev/null and b/examples/knowledgelayer-hello-world/KL-example-readme-graphic.png differ diff --git a/examples/knowledgelayer-hello-world/README.md b/examples/knowledgelayer-hello-world/README.md new file mode 100644 index 0000000..da5f8b2 --- /dev/null +++ b/examples/knowledgelayer-hello-world/README.md @@ -0,0 +1,65 @@ +# Knowledge Layer "Hello World" Use Case: Driving Style Detector + +## Why? + +This repository demonstrates a simple "Hello World" application of the Knowledge Layer, whose joint development was [proposed](https://wiki.covesa.global/pages/viewpage.action?pageId=71074417) in the COVESA Data Architecture working group. Given the generic nature of the underlying architecture, this use case can serve as a starting point for implementing more complex scenarios across various domains. One major advantage is that the use case is built on logical [playground components](../../cdsp/README.md). These components can often be swapped out (based on availability of alternatives) without significantly impacting other components (sometimes even not at all, e.g., whether using [RealmDB](../../cdsp/information-layer/handlers/src/realmdb/README.md) or [IoTDB](../../cdsp/information-layer/handlers/src/iotdb/README.md), it does not affect the Knowledge Layer). + +### Key Aspects Illustrated: +- **Standardized Data Models:** Using models like [VSS](https://github.com/COVESA/vehicle_signal_specification/) and associated tools enables scalable, generic transformation of data into other formats, like graph data formats. +- **Logic Mapping in Data-Driven Rules:** Instead of hardcoded IF-ELSE logic, data-driven rules like [Datalog](https://de.wikipedia.org/wiki/Datalog) offer: + - Easier maintenance of logic + - Functional logic without the need for compilation + - Smaller size for necessary updates + - Easier traceability of decisions through declarative logic description + - Centralized executable knowledge instead of scattered functional logic +- **No Over-Reliance on Machine Learning:** AI is not only Machine Learning, and Machine Learning isn't always the solution. +- **Uniform Implementation Pattern:** Offers a domain-independent pattern for other use cases. + +## What? + +In this use case, we have implemented an AI-powered solution to detect aggressive driving behaviors. Rather than relying on complex, maintenance-intensive IF-ELSE logic in code or overly sophisticated machine learning models, the detection is achieved through straightforward, data-driven rules. + +## How? + +Live VSS data from the current drive made accesible in the [Information Layer](../../cdsp/information-layer/README.md) via [Websocket](../../cdsp/information-layer/router/src/websocket-server.js) are converted by a [JSON-RDF-Convertor](../../cdsp/knowledge-layer/connector/README.md) in real-time into a graph data format ([RDF](https://www.w3.org/RDF/)) and stored within the Knowledge Layer in a [Knowledge Graph](https://en.wikipedia.org/wiki/Knowledge_Graph). At any point, every data point (needed for the use case) in the Information Layer has a graph representation. This data representation allows us to attach a [symbolic reasoner](../../cdsp/knowledge-layer/symbolic-reasoner/README.md) ([RDFox](../../cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md)) to the Knowledge Graph, which can link, evaluate, and infer new facts based on rules, such as deriving the driving style. As soon as an aggressive driving style is detected, the result is converted back from the graph data format to an Informtion Layer tree format in real time and, in our case, stored in the appropriate data field in the VSS tree. The information "aggressive driving style" can then be shared with other interested applications for example via a data sync middleware. + +### Implementation Details + +#### Logical Components and Implementation Decisions: +- **Data Model:** [VSS](https://github.com/COVESA/vehicle_signal_specification/) - Describes vehicle data in a standardized format +- **Feeder/Simulator:** [Remotive Labs](../../examples/remotivelabs-feeder/README.md) - Provides raw data for a test drive +- **Data Storage:** [RealmDB](../../cdsp/information-layer/handlers/src/realmdb/README.md) - Stores live data +- **Data Access:** [WebSocket Interface](../../cdsp/information-layer/router/src/websocket-server.js) - Read, +write, subscribe to VSS data +- **Knowledge Layer Connector:** [RDFConvertor](../../cdsp/knowledge-layer/connector/README.md) - Converts VSS JSON format into RDF Graph format and vice versa +- **Graph Data Storage:** [RDFox](../../cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md) - Stores the transformed and newly generated graph data +- **Rules Language:** [Datalog](https://de.wikipedia.org/wiki/Datalog) - Allows describing IF-ELSE like rules in data-near language +- **Data-Reasoner:** [RDFox](../../cdsp/knowledge-layer/symbolic-reasoner/rdfox/README.md) - Reasons based on rules and graph data, potentially inferring new graph data + +![The Use Case in a DIKW,logical and implementation view](KL-example-readme-graphic.png) + +#### Input, Use Case Logic and Output: + +Our input data for the use case includes: +- `Vehicle.Chassis.SteeringWheel.Angle`: The current angle of the steering wheel. +- `Vehicle.CurrentLocation.Latitude`: The latitude of the vehicle's current location. +- `Vehicle.CurrentLocation.Longitude`: The longitude of the vehicle's current location. +- `Vehicle.Powertrain.TractionBattery.NominalVoltage`: The nominal voltage of the vehicle's traction battery. +- `Vehicle.Powertrain.TractionBattery.StateOfCharge.CurrentEnergy`: The current energy level of the traction battery. +- `Vehicle.Powertrain.Transmission.CurrentGear`: The current gear of the vehicle's transmission. +- `Vehicle.Speed`: The current speed of the vehicle. + +The core logic of our use case is represented by the following rule (here in natural language for better readability): + +
If Vehicle.Chassis.SteeringWheel.Angle changes by more than 90 degrees in less than 3 seconds and Vehicle.Speed is greater than 50 km/h, then flag as aggressive driving.
+ +Derived output data: +- `Vehicle.drivingStyle`: True, when driving style is aggressive, otherwise false + +## Installation and Running + +To be determined (TBD). + +--- + +Feel free to explore the components and rules used in this "Hello World" example to understand the basic implementation of the Knowledge Layer. You can extend this use case to more complex scenarios and even adapt it to different domains. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..51b9d45 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "covesa.cdsp", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}