diff --git a/.gitignore b/.gitignore index befcd8c9..85d5aa5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/.DS_Store **/.vscode +**/.idea bin obj node_modules diff --git a/e2e-tests/07-post-outputbinding-scenario/post-dapr-outputbinding.test.js b/e2e-tests/07-post-outputbinding-scenario/post-dapr-outputbinding.test.js new file mode 100644 index 00000000..a3706c4c --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/post-dapr-outputbinding.test.js @@ -0,0 +1,195 @@ +/** + * Copyright 2024 The Drasi Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const deployResources = require("../fixtures/deploy-resources"); +const deleteResources = require("../fixtures/delete-resources"); +const yaml = require("js-yaml"); +const fs = require("fs"); +const pg = require("pg"); +const redis = require("redis"); +const PortForward = require("../fixtures/port-forward"); +const { waitFor } = require('../fixtures/infrastructure'); // Corrected import path + +// Define paths to your resource files +const resourcesFilePath = __dirname + '/resources.yaml'; +const reactionProviderFilePath = __dirname + '/reaction-provider.yaml'; +const sourcesFilePath = __dirname + '/sources.yaml'; +const queriesFilePath = __dirname + '/queries.yaml'; +const reactionsFilePath = __dirname + '/reactions.yaml'; + +let resourcesToCleanup = []; + +let dbPortForward; +let dbClient; + +let productRedisPortForward, inventoryRedisPortForward; +let productRedisClient, inventoryRedisClient; + +// Function to get state from Redis Hash +// Attempts to parse the value as JSON, but returns raw value if parsing fails +async function getStateFromRedis(redisClient, key) { + try { + if (!redisClient || !redisClient.isOpen) { + console.error(`Redis client for key "${key}" is not open or not initialized.`); + return null; + } + console.log(`Attempting to get state for key "${key}" from Redis...`); + + let rawValue = await redisClient.hGet(key, "data"); + + if (rawValue !== null) { + console.log(`Raw value for key "${key}" (from HGET key "data"): ${rawValue}`); + } else { + console.log(`Key "${key}" not found or has no parsable data in Redis.`); + return null; + } + + try { + return JSON.parse(rawValue); + } catch (e) { + console.error(`Failed to parse Redis value for key "${key}":`, rawValue, e); + return rawValue; + } + } catch (error) { + console.error(`Error during getStateFromRedis for key "${key}":`, error.message, error.stack); + return null; + } +} + +beforeAll(async () => { + // Load resources from resources.yaml + const infraResources = yaml.loadAll(fs.readFileSync(resourcesFilePath, 'utf8')); + // Load the reaction-provider + const reactionProviderResources = yaml.loadAll(fs.readFileSync(reactionProviderFilePath, 'utf8')); + // Load Drasi source + const sources = yaml.loadAll(fs.readFileSync(sourcesFilePath, 'utf8')); + // Load Drasi Query + const queries = yaml.loadAll(fs.readFileSync(queriesFilePath, 'utf8')); + // Load Drasi Reactions + const reactions = yaml.loadAll(fs.readFileSync(reactionsFilePath, 'utf8')); + + // Combine all resources to be deployed + resourcesToCleanup = [...infraResources, ...reactionProviderResources, ...sources, ...queries, ...reactions]; + + console.log(`Deploying ${infraResources.length} infra resources...`); + await deployResources(infraResources); + + console.log("Waiting for infra resources to initialize..."); + await new Promise(r => setTimeout(r, 30000)); + + console.log(`Deploying ${sources.length} sources...`); + await deployResources(sources); + + console.log(`Deploying ${queries.length} queries...`); + await deployResources(queries); + + console.log(`Deploying PostOutputBinding reaction provider...`) + await deployResources(reactionProviderResources); + + console.log(`Deploying ${reactions.length} reactions...`); + await deployResources(reactions); + + // Setup PostgreSQL client + dbPortForward = new PortForward("product-inventory-db", 5432, "default"); + const dbPort = await dbPortForward.start(); + dbClient = new pg.Client({ + user: "postgres", + password: "postgres", + host: "localhost", + port: dbPort, + database: "productdb", + }); + await dbClient.connect(); + console.log("Connected to PostgreSQL, with port forwarded at", dbPort); + + // Setup Redis clients + productRedisPortForward = new PortForward("redis-product", 6379, "default"); + const productRedisPort = await productRedisPortForward.start(); + productRedisClient = redis.createClient({ url: `redis://localhost:${productRedisPort}` }); + await productRedisClient.connect(); + console.log("Connected to Product Redis, with port forwarded at", productRedisPort); + + inventoryRedisPortForward = new PortForward("redis-inventory", 6379, "default"); + const inventoryRedisPort = await inventoryRedisPortForward.start(); + inventoryRedisClient = redis.createClient({ url: `redis://localhost:${inventoryRedisPort}` }); + await inventoryRedisClient.connect(); + console.log("Connected to Inventory Redis, with port forwarded at", inventoryRedisPort); + + await waitFor({ timeout: 15000, description: "initial propagation after setup" }) + + console.log("Setup complete."); +}, 480000); + +afterAll(async () => { + if (dbClient) { + await dbClient.end(); + console.log("PostgreSQL client disconnected."); + } + + if (dbPortForward) { + dbPortForward.stop(); + console.log("PostgreSQL port forward stopped."); + } + + if (productRedisClient) { + await productRedisClient.quit(); + console.log("Product Redis client disconnected."); + } + if (productRedisPortForward) { + productRedisPortForward.stop(); + console.log("Product Redis port forward stopped."); + } + + if (inventoryRedisClient) { + await inventoryRedisClient.quit(); + console.log("Inventory Redis client disconnected."); + } + if (inventoryRedisPortForward) { + inventoryRedisPortForward.stop(); + console.log("Inventory Redis port forward stopped."); + } + + if (resourcesToCleanup.length > 0) { + console.log(`Deleting ${resourcesToCleanup.length} resources...`); + await deleteResources(resourcesToCleanup); + console.log("Teardown complete."); + } +}); + +describe("Dapr OutputBinding Reaction Test Suite", () => { + test("UNPACKED: should sync the initial state to Dapr statestore with create", async () => { + console.log("Verifying initial state sync for Product data..."); + const newProductName = `Test Unpacked Packed ${Date.now()}`; + const newProductPrice = 99.99; + await dbClient.query( + "INSERT INTO product (name, description, price) VALUES ($1, 'Unpacked Test Desc', $2)", + [newProductName, newProductPrice] + ); + + const receivedMessage = await waitFor({ + actionFn: () => productRedisClient.get('inventory'), + predicateFn: (messages) => messages && messages.length >= 1, + timeoutMs: 10000, + pollIntervalMs: 1000, + description: `unpacked message for product "${newProductName}" to appear in Redis` + }); + // 1. Verify Product Data (product-statestore) + expect(receivedMessage).toBeDefined(); + // JSON Stringify the keys to log them + console.log("Received keys from Redis:", JSON.stringify(receivedMessage)); + expect(receivedMessage).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/e2e-tests/07-post-outputbinding-scenario/queries.yaml b/e2e-tests/07-post-outputbinding-scenario/queries.yaml new file mode 100644 index 00000000..789a9ca0 --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/queries.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: ContinuousQuery +name: product-query +spec: + mode: query + sources: + subscriptions: + - id: product-inventory-source + query: > + MATCH + (p:product) + RETURN + p.product_id AS product_id, + p.name AS product_name, + p.description AS product_description +--- +apiVersion: v1 +kind: ContinuousQuery +name: inventory-query +spec: + mode: query + sources: + subscriptions: + - id: product-inventory-source + nodes: + - sourceLabel: inventory + - sourceLabel: product + joins: + - id: INVENTORY_FOR_PRODUCT + keys: + - label: inventory + property: product_id + - label: product + property: product_id + query: > + MATCH + (i:inventory)-[:INVENTORY_FOR_PRODUCT]->(p:product) + RETURN + i.inventory_id AS inventory_id, + i.product_id AS product_id, + i.quantity AS product_quantity, + i.location AS product_location, + p.name AS product_name, + p.description AS product_description diff --git a/e2e-tests/07-post-outputbinding-scenario/reaction-provider.yaml b/e2e-tests/07-post-outputbinding-scenario/reaction-provider.yaml new file mode 100644 index 00000000..7cb3bd80 --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/reaction-provider.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ReactionProvider +name: PostDaprOutputBinding +spec: + services: + reaction: + image: reaction-post-dapr-output-binding + config_schema: + type: object + diff --git a/e2e-tests/07-post-outputbinding-scenario/reactions.yaml b/e2e-tests/07-post-outputbinding-scenario/reactions.yaml new file mode 100644 index 00000000..021f4255 --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/reactions.yaml @@ -0,0 +1,30 @@ +kind: Reaction +apiVersion: v1 +name: sync-dapr-outputbinding +spec: + kind: PostDaprOutputBinding + queries: + product-query: > + { + "bindingName": "product-outputbinding", + "bindingType": "redis", + "bindingOperation": "create", + "bindingMetadataTemplate": { + "key": "{{payload.after.product_name}}" + }, + "packed": "Unpacked", + "maxFailureCount": 5, + "skipControlSignals": true + } + inventory-query: > + { + "bindingName": "inventory-outputbinding", + "bindingType": "redis", + "bindingOperation": "create", + "bindingMetadataTemplate": { + "key": "inventory" + }, + "packed": "Unpacked", + "maxFailureCount": 5, + "skipControlSignals": true + } \ No newline at end of file diff --git a/e2e-tests/07-post-outputbinding-scenario/resources.yaml b/e2e-tests/07-post-outputbinding-scenario/resources.yaml new file mode 100644 index 00000000..d8c1d430 --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/resources.yaml @@ -0,0 +1,305 @@ +# PostgreSQL ConfigMap for Initialization +apiVersion: v1 +kind: ConfigMap +metadata: + name: product-inventory-db-init + labels: + app: product-inventory-db +data: + init.sql: | + CREATE ROLE replication_group; + CREATE ROLE replication_user REPLICATION LOGIN; + GRANT replication_group TO postgres; + GRANT replication_group TO replication_user; + + CREATE TABLE IF NOT EXISTS product ( + product_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(10, 2) + ); + + CREATE TABLE IF NOT EXISTS inventory ( + inventory_id SERIAL PRIMARY KEY, + product_id INTEGER NOT NULL REFERENCES product(product_id) ON DELETE CASCADE, + quantity INTEGER NOT NULL, + location VARCHAR(100) + ); + + -- Set table ownership to the replication_group + ALTER TABLE product OWNER TO replication_group; + ALTER TABLE inventory OWNER TO replication_group; + + -- Insert initial data + INSERT INTO product (name, description, price) VALUES + ('SuperWidget', 'An amazing widget with all the features.', 19.99), + ('MegaGadget', 'The biggest gadget you have ever seen.', 29.99), + ('TinyThing', 'A small but powerful thing.', 9.99) + ON CONFLICT (product_id) DO NOTHING; + + INSERT INTO inventory (product_id, quantity, location) VALUES + ((SELECT product_id from product WHERE name = 'SuperWidget'), 100, 'Warehouse A'), + ((SELECT product_id from product WHERE name = 'MegaGadget'), 50, 'Warehouse B'), + ((SELECT product_id from product WHERE name = 'TinyThing'), 200, 'Warehouse A'), + ((SELECT product_id from product WHERE name = 'SuperWidget'), 75, 'Warehouse C') + ON CONFLICT (inventory_id) DO NOTHING; + + -- Create a publication for the tables + CREATE PUBLICATION product_inventory_publication FOR TABLE product, inventory; + + -- Create a replication slot + SELECT pg_create_logical_replication_slot('product_inventory_slot', 'pgoutput'); +--- +# PostgreSQL Secret for Credentials +apiVersion: v1 +kind: Secret +metadata: + name: product-inventory-db-credentials + labels: + app: product-inventory-db +type: Opaque +stringData: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres +--- +# PostgreSQL StatefulSet +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: product-inventory-db + labels: + app: product-inventory-db +spec: + serviceName: product-inventory-db + replicas: 1 + selector: + matchLabels: + app: product-inventory-db + template: + metadata: + labels: + app: product-inventory-db + spec: + containers: + - name: postgres + image: postgres:14 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 5432 + name: postgres + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: product-inventory-db-credentials + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: product-inventory-db-credentials + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: productdb + args: + - -c + - wal_level=logical + - -c + - max_replication_slots=10 + - -c + - max_wal_senders=10 + volumeMounts: + - name: product-inventory-db-data + mountPath: /var/lib/postgresql/data + - name: init-script + mountPath: /docker-entrypoint-initdb.d + resources: + limits: + cpu: "1" + memory: "1Gi" + requests: + cpu: "0.5" + memory: "512Mi" + volumes: + - name: product-inventory-db-data + emptyDir: {} + - name: init-script + configMap: + name: product-inventory-db-init +--- +# PostgreSQL Service +apiVersion: v1 +kind: Service +metadata: + name: product-inventory-db + labels: + app: product-inventory-db +spec: + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + name: postgres + selector: + app: product-inventory-db + type: ClusterIP +--- +# Redis Deployment for Product State +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis-product + labels: + app: redis-product +spec: + replicas: 1 + selector: + matchLabels: + app: redis-product + template: + metadata: + labels: + app: redis-product + dapr.io/enabled: "true" + dapr.io/app-id: "redis-product" + dapr.io/app-port: "6379" + spec: + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + name: redis + resources: + limits: + cpu: "0.5" + memory: "256Mi" + requests: + cpu: "0.1" + memory: "128Mi" +--- +# Redis Service +apiVersion: v1 +kind: Service +metadata: + name: redis-product + labels: + app: redis-product +spec: + ports: + - port: 6379 + targetPort: 6379 + protocol: TCP + name: redis + selector: + app: redis-product + type: ClusterIP +--- +# Dapr Output Binding Component for Redis Output Binding (default namespace) +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: product-outputbinding +spec: + type: bindings.redis + version: v1 + metadata: + - name: redisHost + value: redis-product.default.svc.cluster.local:6379 + - name: redisPassword + value: "" +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: product-outputbinding + namespace: drasi-system +spec: + type: bindings.redis + version: v1 + metadata: + - name: redisHost + value: redis-product.default.svc.cluster.local:6379 + - name: redisPassword + value: "" +--- +# Redis Deployment for redis-inventory +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis-inventory + labels: + app: redis-inventory +spec: + replicas: 1 + selector: + matchLabels: + app: redis-inventory + template: + metadata: + labels: + app: redis-inventory + dapr.io/enabled: "true" + dapr.io/app-id: "redis-inventory" + dapr.io/app-port: "6379" + spec: + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + name: redis + resources: + limits: + cpu: "0.5" + memory: "256Mi" + requests: + cpu: "0.1" + memory: "128Mi" +--- +# Redis Service for Inventory State +apiVersion: v1 +kind: Service +metadata: + name: redis-inventory + labels: + app: redis-inventory +spec: + ports: + - port: 6379 + targetPort: 6379 + protocol: TCP + name: redis + selector: + app: redis-inventory + type: ClusterIP +--- +# Dapr Output Binding Component for Http Inventory in Drasi Namespace +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: inventory-outputbinding +spec: + type: bindings.redis + version: v1 + metadata: + - name: redisHost + value: redis-product.default.svc.cluster.local:6379 + - name: redisPassword + value: "" +--- +# Dapr Output Binding Component for Http Inventory in Drasi Namespace +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: inventory-outputbinding + namespace: drasi-system +spec: + type: bindings.redis + version: v1 + metadata: + - name: redisHost + value: redis-product.default.svc.cluster.local:6379 + - name: redisPassword + value: "" diff --git a/e2e-tests/07-post-outputbinding-scenario/sources.yaml b/e2e-tests/07-post-outputbinding-scenario/sources.yaml new file mode 100644 index 00000000..1fbb725b --- /dev/null +++ b/e2e-tests/07-post-outputbinding-scenario/sources.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Source +name: product-inventory-source +spec: + kind: PostgreSQL + properties: + host: product-inventory-db.default.svc.cluster.local + port: 5432 + user: postgres + password: postgres + database: productdb + ssl: false + tables: + - public.product + - public.inventory \ No newline at end of file diff --git a/e2e-tests/README.md b/e2e-tests/README.md index a7ec6fa8..b5a444e6 100644 --- a/e2e-tests/README.md +++ b/e2e-tests/README.md @@ -5,7 +5,7 @@ - [Kind](https://kind.sigs.k8s.io/) - Node - Build the Drasi CLI and add it to your path - +- Having build all images locally by running ```bash make docker-build``` at project root ## Running ```bash diff --git a/e2e-tests/fixtures/infrastructure.js b/e2e-tests/fixtures/infrastructure.js index 612baa6c..6c9a3ec7 100644 --- a/e2e-tests/fixtures/infrastructure.js +++ b/e2e-tests/fixtures/infrastructure.js @@ -40,6 +40,7 @@ const images = [ "drasi-project/reaction-storedproc", "drasi-project/reaction-gremlin", "drasi-project/reaction-sync-dapr-statestore", + "drasi-project/reaction-post-dapr-output-binding", "drasi-project/reaction-post-dapr-pubsub", ]; diff --git a/e2e-tests/package.json b/e2e-tests/package.json index f7363ddf..12757569 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -42,5 +42,6 @@ "eslint-plugin-jest": "^27.2.1", "jest": "^29.7.0", "redis": "^4.7.1" - } + }, + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" } diff --git a/e2e-tests/pnpm-lock.yaml b/e2e-tests/pnpm-lock.yaml new file mode 100644 index 00000000..1496d243 --- /dev/null +++ b/e2e-tests/pnpm-lock.yaml @@ -0,0 +1,3816 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@microsoft/signalr': + specifier: ^7.0.2 + version: 7.0.14 + gremlin: + specifier: ^3.7.3 + version: 3.7.3 + jest-junit: + specifier: ^16.0.0 + version: 16.0.0 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + knex: + specifier: ^2.4.2 + version: 2.5.1(pg@8.16.0) + path: + specifier: ^0.12.7 + version: 0.12.7 + pg: + specifier: ^8.9.0 + version: 8.16.0 + portfinder: + specifier: ^1.0.32 + version: 1.0.37 + tough-cookie: + specifier: ^4.1.3 + version: 4.1.4 + devDependencies: + '@jest/globals': + specifier: ^29.4.1 + version: 29.7.0 + '@types/jest': + specifier: ^29.4.0 + version: 29.5.14 + eslint: + specifier: ^8.33.0 + version: 8.57.1 + eslint-plugin-jest: + specifier: ^27.2.1 + version: 27.9.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29))(typescript@5.8.3) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.29) + redis: + specifier: ^4.7.1 + version: 4.7.1 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.3': + resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.3': + resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.4': + resolution: {integrity: sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.4': + resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.3': + resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + 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 + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + 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 + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@microsoft/signalr@7.0.14': + resolution: {integrity: sha512-dnS7gSJF5LxByZwJaj82+F1K755ya7ttPT+JnSeCBef3sL8p8FBkHePXphK8NSuOquIb7vsphXWa28A+L2SPpw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.15.29': + resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001720: + resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + electron-to-chromium@1.5.161: + resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-jest@27.9.0: + resolution: {integrity: sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 || ^7.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gremlin@3.7.3: + resolution: {integrity: sha512-Mn12HDmSlT2gFzNbVD4vlIdsKJLqKNomu1LbJyXWVgBFl2STHp1F5XzvZVu44Y2K/OpHy/p/B2kHdEzDyQq3XA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + 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 + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-junit@16.0.0: + resolution: {integrity: sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==} + engines: {node: '>=10.12.0'} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + knex@2.5.1: + resolution: {integrity: sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + + pg-cloudflare@1.2.5: + resolution: {integrity: sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==} + + pg-connection-string@2.6.1: + resolution: {integrity: sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==} + + pg-connection-string@2.9.0: + resolution: {integrity: sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.10.0: + resolution: {integrity: sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.0: + resolution: {integrity: sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.0: + resolution: {integrity: sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + portfinder@1.0.37: + resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} + engines: {node: '>= 10.12'} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + redis@4.7.1: + resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + 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 + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.3': {} + + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.4 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.3': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + + '@babel/parser@7.27.4': + dependencies: + '@babel/types': 7.27.3 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.3': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + 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': 22.15.29 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.15.29) + 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.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.15.29 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + 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.25 + '@types/node': 22.15.29 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.27.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.15.29 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@microsoft/signalr@7.0.14': + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.3 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.15.29 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/node@22.15.29': + dependencies: + undici-types: 6.21.0 + + '@types/semver@7.7.0': {} + + '@types/stack-utils@2.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.1 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.2 + tsutils: 3.21.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async@3.2.6: {} + + babel-jest@29.7.0(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.27.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.4) + + babel-preset-jest@29.6.3(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001720 + electron-to-chromium: 1.5.161 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001720: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cluster-key-slot@1.1.2: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.19: {} + + commander@10.0.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + create-jest@29.7.0(@types/node@22.15.29): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.15.29) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + dedent@1.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + electron-to-chromium@1.5.161: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-jest@27.9.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29))(typescript@5.8.3): + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3) + eslint: 8.57.1 + optionalDependencies: + jest: 29.7.0(@types/node@22.15.29) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + esm@3.2.25: {} + + espree@9.6.1: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + eventsource@2.0.2: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + 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 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fetch-cookie@2.2.0: + dependencies: + set-cookie-parser: 2.7.1 + tough-cookie: 4.1.4 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generic-pool@3.9.0: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-package-type@0.1.0: {} + + get-stream@6.0.1: {} + + getopts@2.3.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gremlin@3.7.3: + dependencies: + buffer: 6.0.3 + eventemitter3: 5.0.1 + readable-stream: 4.7.0 + uuid: 9.0.1 + ws: 8.18.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + human-signals@2.1.0: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + interpret@2.2.0: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.27.4 + '@babel/parser': 7.27.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.27.4 + '@babel/parser': 7.27.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0 + is-generator-fn: 2.1.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.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.15.29): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.15.29) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.15.29) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.15.29): + dependencies: + '@babel/core': 7.27.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.29 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.15.29 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-junit@16.0.0: + dependencies: + mkdirp: 1.0.4 + strip-ansi: 6.0.1 + uuid: 8.3.2 + xml: 1.0.1 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + 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': 22.15.29 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + 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': 22.15.29 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.27.4 + '@babel/generator': 7.27.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/types': 7.27.3 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + 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.7.2 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.15.29 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.15.29): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.15.29) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + knex@2.5.1(pg@8.16.0): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.1 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + pg: 8.16.0 + transitivePeerDependencies: + - supports-color + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + mkdirp@1.0.4: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-int64@0.4.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + + pg-cloudflare@1.2.5: + optional: true + + pg-connection-string@2.6.1: {} + + pg-connection-string@2.9.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.10.0(pg@8.16.0): + dependencies: + pg: 8.16.0 + + pg-protocol@1.10.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.0: + dependencies: + pg-connection-string: 2.9.0 + pg-pool: 3.10.0(pg@8.16.0) + pg-protocol: 1.10.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.5 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + portfinder@1.0.37: + dependencies: + async: 3.2.6 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prelude-ls@1.2.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process@0.11.10: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.10 + + redis@4.7.1: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) + + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tarn@3.0.2: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + tildify@2.0.0: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + tslib@1.14.1: {} + + tsutils@3.21.0(typescript@5.8.3): + dependencies: + tslib: 1.14.1 + typescript: 5.8.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@7.5.10: {} + + ws@8.18.2: {} + + xml@1.0.1: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/query-container/query-host/drasi-core b/query-container/query-host/drasi-core index 114039f0..f6f0be23 160000 --- a/query-container/query-host/drasi-core +++ b/query-container/query-host/drasi-core @@ -1 +1 @@ -Subproject commit 114039f0a5364041c5876caa66640674c0dc45dd +Subproject commit f6f0be238f9e3687e0cc1d8c26d7e8b61fa55be7 diff --git a/reactions/Makefile b/reactions/Makefile index 0173e3e7..ae607911 100644 --- a/reactions/Makefile +++ b/reactions/Makefile @@ -12,6 +12,7 @@ docker-build: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) @@ -24,6 +25,7 @@ docker-build-debug: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) @@ -36,6 +38,7 @@ kind-load: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) @@ -49,6 +52,7 @@ k3d-load: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) @@ -61,6 +65,7 @@ test: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) @@ -73,6 +78,7 @@ lint-check: $(MAKE) -C azure/eventgrid-reaction $(MAKECMDGOALS) $(MAKE) -C azure/storagequeue-reaction $(MAKECMDGOALS) $(MAKE) -C dapr/sync-statestore $(MAKECMDGOALS) + $(MAKE) -C dapr/post-output-binding $(MAKECMDGOALS) $(MAKE) -C sql/storedproc-reaction $(MAKECMDGOALS) $(MAKE) -C gremlin/gremlin-reaction $(MAKECMDGOALS) $(MAKE) -C debezium/debezium-reaction $(MAKECMDGOALS) diff --git a/reactions/dapr/post-output-binding/Dockerfile b/reactions/dapr/post-output-binding/Dockerfile new file mode 100644 index 00000000..944bb062 --- /dev/null +++ b/reactions/dapr/post-output-binding/Dockerfile @@ -0,0 +1,32 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# Copy solution and project files +COPY post-dapr-output-binding.sln ./ +COPY Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj ./Drasi.Reactions.PostDaprOutputBinding/ + +# Restore dependencies +RUN dotnet restore "./Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj" + +# Copy only the source code +COPY Drasi.Reactions.PostDaprOutputBinding/ ./Drasi.Reactions.PostDaprOutputBinding/ + +# Build the reaction project +WORKDIR /src/Drasi.Reactions.PostDaprOutputBinding +RUN dotnet publish "./Drasi.Reactions.PostDaprOutputBinding.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app +COPY --from=build /app/publish . + +# Set log levels for reaction in production +ENV Logging__LogLevel__Default="Debug" +ENV Logging__LogLevel__Microsoft="Warning" +ENV Logging__LogLevel__Microsoft_Hosting_Lifetime="Information" +ENV Logging__LogLevel__Drasi_Reactions_PostDaprOutputBinding="Debug" + +USER app +ENTRYPOINT ["dotnet", "Drasi.Reactions.PostDaprOutputBinding.dll"] \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Dockerfile.debug b/reactions/dapr/post-output-binding/Dockerfile.debug new file mode 100644 index 00000000..527052a4 --- /dev/null +++ b/reactions/dapr/post-output-binding/Dockerfile.debug @@ -0,0 +1,33 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# Copy solution and project files +COPY post-dapr-output-binding.sln ./ +COPY Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj ./Drasi.Reactions.PostDaprOutputBinding/ + +# Restore dependencies +RUN dotnet restore "./Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj" + +# Copy only the source code +COPY Drasi.Reactions.PostDaprOutputBinding/ ./Drasi.Reactions.PostDaprOutputBinding/ + +# Build the reaction project +WORKDIR /src/Drasi.Reactions.PostDaprOutputBinding +RUN dotnet publish "./Drasi.Reactions.PostDaprOutputBinding.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Final stage/image +FROM ubuntu:25.04 AS final +RUN apt-get update && apt-get install -y bash curl dotnet-runtime-8.0 aspnetcore-runtime-8.0 && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=build /app/publish . + +# Set log levels for reaction in debug environment +ENV Logging__LogLevel__Default="Debug" +ENV Logging__LogLevel__Microsoft="Information" +ENV Logging__LogLevel__Microsoft_Hosting_Lifetime="Information" +ENV Logging__LogLevel__Drasi_Reactions_PostDaprPubSub="Debug" + +USER app +ENTRYPOINT ["dotnet", "Drasi.Reactions.PostDaprOutputBinding.dll"] \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeFormatterTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeFormatterTests.cs new file mode 100644 index 00000000..83309a81 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeFormatterTests.cs @@ -0,0 +1,183 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Drasi.Reaction.SDK.Models.QueryOutput; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Moq; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class ChangeFormatterTests +{ + [Fact] + public void DrasiChangeFormatter_FormatEmptyChangeEvent_ShouldReturnEmptyCollection() + { + // Arrange + var formatter = new DrasiChangeFormatter(); + var evt = new ChangeEvent + { + QueryId = "test-query", + Sequence = 1, + AddedResults = Array.Empty>(), + UpdatedResults = Array.Empty(), + DeletedResults = Array.Empty>() + }; + + // Act + var result = formatter.Format(evt); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void DrasiChangeFormatter_FormatAddedResults_ShouldReturnCorrectFormat() + { + // Arrange + var formatter = new DrasiChangeFormatter(); + var addedItem = new Dictionary { { "id", "123" }, { "name", "test" } }; + var evt = new ChangeEvent + { + QueryId = "test-query", + Sequence = 1, + SourceTimeMs = 1000, + AddedResults = new[] { addedItem }, + UpdatedResults = Array.Empty(), + DeletedResults = Array.Empty>() + }; + + // Act + var result = formatter.Format(evt).ToList(); + + // Assert + Assert.Single(result); + var json = result[0].GetRawText(); + Assert.Contains("\"op\":\"i\"", json); + Assert.Contains("\"queryId\":\"test-query\"", json); + Assert.Contains("\"id\":\"123\"", json); + Assert.Contains("\"name\":\"test\"", json); + } + + [Fact] + public void DrasiChangeFormatter_FormatUpdatedResults_ShouldReturnCorrectFormat() + { + // Arrange + var formatter = new DrasiChangeFormatter(); + var beforeItem = new Dictionary { { "id", "123" }, { "name", "before" } }; + var afterItem = new Dictionary { { "id", "123" }, { "name", "after" } }; + var updatedElement = new UpdatedResultElement { Before = beforeItem, After = afterItem }; + + var evt = new ChangeEvent + { + QueryId = "test-query", + Sequence = 1, + SourceTimeMs = 1000, + AddedResults = Array.Empty>(), + UpdatedResults = new[] { updatedElement }, + DeletedResults = Array.Empty>() + }; + + // Act + var result = formatter.Format(evt).ToList(); + + // Assert + Assert.Single(result); + var json = result[0].GetRawText(); + Assert.Contains("\"op\":\"u\"", json); + Assert.Contains("\"queryId\":\"test-query\"", json); + Assert.Contains("\"before\":{", json); + Assert.Contains("\"after\":{", json); + Assert.Contains("\"name\":\"before\"", json); + Assert.Contains("\"name\":\"after\"", json); + } + + [Fact] + public void DrasiChangeFormatter_FormatDeletedResults_ShouldReturnCorrectFormat() + { + // Arrange + var formatter = new DrasiChangeFormatter(); + var deletedItem = new Dictionary { { "id", "123" }, { "name", "test" } }; + var evt = new ChangeEvent + { + QueryId = "test-query", + Sequence = 1, + SourceTimeMs = 1000, + AddedResults = Array.Empty>(), + UpdatedResults = Array.Empty(), + DeletedResults = new[] { deletedItem } + }; + + // Act + var result = formatter.Format(evt).ToList(); + + // Assert + Assert.Single(result); + var json = result[0].GetRawText(); + Assert.Contains("\"op\":\"d\"", json); + Assert.Contains("\"queryId\":\"test-query\"", json); + Assert.Contains("\"before\":{", json); + Assert.Contains("\"id\":\"123\"", json); + Assert.DoesNotContain("\"after\":{", json); + } + + [Fact] + public void DrasiChangeFormatter_FormatMultipleResults_ShouldReturnCorrectCount() + { + // Arrange + var formatter = new DrasiChangeFormatter(); + var addedItem = new Dictionary { { "id", "123" }, { "name", "test" } }; + var deletedItem = new Dictionary { { "id", "456" }, { "name", "deleted" } }; + var beforeItem = new Dictionary { { "id", "789" }, { "name", "before" } }; + var afterItem = new Dictionary { { "id", "789" }, { "name", "after" } }; + var updatedElement = new UpdatedResultElement { Before = beforeItem, After = afterItem }; + + var evt = new ChangeEvent + { + QueryId = "test-query", + Sequence = 1, + SourceTimeMs = 1000, + AddedResults = new[] { addedItem }, + UpdatedResults = new[] { updatedElement }, + DeletedResults = new[] { deletedItem } + }; + + // Act + var result = formatter.Format(evt).ToList(); + + // Assert + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.GetRawText().Contains("\"op\":\"i\"")); + Assert.Contains(result, r => r.GetRawText().Contains("\"op\":\"u\"")); + Assert.Contains(result, r => r.GetRawText().Contains("\"op\":\"d\"")); + } + + [Fact] + public void ChangeFormatterFactory_GetFormatter_ShouldReturnDrasiFormatter() + { + // Arrange + var serviceProvider = new Mock(); + var drasiFormatter = new DrasiChangeFormatter(); + + serviceProvider.Setup(s => s.GetService(typeof(DrasiChangeFormatter))).Returns(drasiFormatter); + + var factory = new ChangeFormatterFactory(serviceProvider.Object); + + // Act + var drasiResult = factory.GetFormatter(); + + // Assert + Assert.Same(drasiFormatter, drasiResult); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeHandlerTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeHandlerTests.cs new file mode 100644 index 00000000..46ccc57f --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ChangeHandlerTests.cs @@ -0,0 +1,370 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text.Json; +using Dapr; +using Dapr.Client; +using Drasi.Reaction.SDK.Models.QueryOutput; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class ChangeHandlerTests +{ + private readonly Mock _mockDaprClient; + private readonly Mock _mockFormatterFactory; + private readonly Mock> _mockLogger; + private readonly Mock _mockFailureTracker; + + private readonly ChangeHandler _handler; + + public ChangeHandlerTests() + { + _mockDaprClient = new Mock(); + _mockFormatterFactory = new Mock(); + _mockLogger = new Mock>(); + _mockFailureTracker = new Mock(); + + _handler = new ChangeHandler( + _mockDaprClient.Object, + _mockFormatterFactory.Object, + _mockLogger.Object, + _mockFailureTracker.Object + ); + } + + [Fact] + public async Task HandleChange_NullConfig_ThrowsArgumentNullException() + { + // Arrange + var evt = new ChangeEvent { QueryId = "test-query" }; + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.HandleChange(evt, null)); + } + + [Fact] + public async Task HandleChange_QueryInFailedState_ThrowsInvalidOperationException() + { + // Arrange + var evt = new ChangeEvent { QueryId = "test-query" }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec" + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(true); + _mockFailureTracker.Setup(ft => ft.GetFailureReason("test-query")).Returns("Test failure reason"); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _handler.HandleChange(evt, config)); + Assert.Contains("test-query", ex.Message); + Assert.Contains("Test failure reason", ex.Message); + } + + [Fact] + public async Task HandleChange_PackedFormat_PublishesPackedEvent() + { + // Arrange + var evt = new ChangeEvent { + QueryId = "test-query", + AddedResults = new[] { new Dictionary { { "id", "1" } } } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleChange(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny() + ), Times.Once); + + _mockFailureTracker.Verify(ft => ft.ResetFailures("test-query"), Times.Once); + } + + [Fact] + public async Task HandleChange_UnpackedFormat_PublishesUnpackedEvents() + { + // Arrange + var evt = new ChangeEvent { + QueryId = "test-query", + AddedResults = new[] { new Dictionary { { "id", "1" } } } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Unpacked // Unpacked is default + }; + + var mockFormatter = new Mock(); + var formattedElements = new[] { + JsonDocument.Parse("{\"test\":\"value\"}").RootElement + }; + + mockFormatter.Setup(f => f.Format(evt)).Returns(formattedElements); + _mockFormatterFactory.Setup(ff => ff.GetFormatter()).Returns(mockFormatter.Object); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleChange(evt, config); + + // Assert + _mockFormatterFactory.Verify(ff => ff.GetFormatter(), Times.Once); + mockFormatter.Verify(f => f.Format(evt), Times.Once); + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny() + ), Times.Once); + + _mockFailureTracker.Verify(ft => ft.ResetFailures("test-query"), Times.Once); + } + + [Fact] + public async Task HandleChange_PublishFails_RecordsFailureAndRethrows() + { + // Arrange + var evt = new ChangeEvent { QueryId = "test-query" }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed, + MaxFailureCount = 3 + }; + + var exception = new DaprException("Test error"); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .ThrowsAsync(exception); + + _mockFailureTracker.Setup(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny())) + .Returns(false); // Not yet failed + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _handler.HandleChange(evt, config)); + Assert.Same(exception, ex); + + _mockFailureTracker.Verify(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny() + ), Times.Once); + } + + [Fact] + public async Task HandleChange_MultipleFailuresExceedingThreshold_MarksQueryAsFailed() + { + // Arrange + var evt = new ChangeEvent { QueryId = "test-query" }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed, + MaxFailureCount = 3 + }; + + var exception = new DaprException("Test error"); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .ThrowsAsync(exception); + + _mockFailureTracker.Setup(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny())) + .Returns(true); // Query is now failed + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.HandleChange(evt, config)); + + _mockFailureTracker.Verify(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny() + ), Times.Once); + } + + [Fact] + public async Task HandleChange_MultipleUnpackedEvents_PublishesEachEvent() + { + // Arrange + var evt = new ChangeEvent { + QueryId = "test-query", + AddedResults = + [ + new Dictionary { { "id", "1" }, { "example", "example_value"} }, + new Dictionary { { "id", "2" } } + ] + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + BindingMetadataTemplate = new Dictionary() + { + { "key1", "{{id}}" } + }, + Packed = OutputFormat.Unpacked + }; + + var mockFormatter = new Mock(); + var formattedElements = new[] { + JsonDocument.Parse("{\"id\":\"1\"}").RootElement, + JsonDocument.Parse("{\"id\":\"2\"}").RootElement + }; + + mockFormatter.Setup(f => f.Format(evt)).Returns(formattedElements); + _mockFormatterFactory.Setup(ff => ff.GetFormatter()).Returns(mockFormatter.Object); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleChange(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + new Dictionary + { + { "key1", "1" } + }, + It.IsAny() + ), Times.Exactly(1)); + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + new Dictionary + { + { "key1", "2" } + }, + It.IsAny() + ), Times.Exactly(1)); + } + + [Fact] + public async Task HandleChange_UnpackedEventExactTemplate_PublishesEachEvent() + { + // Arrange + var evt = new ChangeEvent { + QueryId = "test-query", + AddedResults = + [ + new Dictionary { { "id", "1" }} + ] + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + BindingMetadataTemplate = new Dictionary() + { + { "key1", "key-1" } + }, + Packed = OutputFormat.Unpacked + }; + + var mockFormatter = new Mock(); + var formattedElements = new[] { + JsonDocument.Parse("{\"id\":\"1\"}").RootElement + }; + + mockFormatter.Setup(f => f.Format(evt)).Returns(formattedElements); + _mockFormatterFactory.Setup(ff => ff.GetFormatter()).Returns(mockFormatter.Object); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleChange(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + new Dictionary + { + { "key1", "key-1" } + }, + It.IsAny() + ), Times.Exactly(1)); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ControlSignalHandlerTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ControlSignalHandlerTests.cs new file mode 100644 index 00000000..1ba47897 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ControlSignalHandlerTests.cs @@ -0,0 +1,329 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text.Json; +using Dapr; +using Dapr.Client; +using Drasi.Reaction.SDK.Models.QueryOutput; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +// Added for QueryConfig + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class ControlSignalHandlerTests +{ + private readonly Mock _mockDaprClient; + private readonly Mock> _mockLogger; + private readonly Mock _mockFailureTracker; + private readonly ControlSignalHandler _handler; + + public ControlSignalHandlerTests() + { + _mockDaprClient = new Mock(); + _mockLogger = new Mock>(); + _mockFailureTracker = new Mock(); + + _handler = new ControlSignalHandler( + _mockDaprClient.Object, + _mockLogger.Object, + _mockFailureTracker.Object + ); + } + + [Fact] + public async Task HandleControlSignal_NullConfig_ThrowsArgumentNullException() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.HandleControlSignal(evt, null!)); + } + + [Fact] + public async Task HandleControlSignal_QueryInFailedState_ThrowsInvalidOperationException() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec" + + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(true); + _mockFailureTracker.Setup(ft => ft.GetFailureReason("test-query")).Returns("Test failure reason"); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _handler.HandleControlSignal(evt, config)); + Assert.Contains("test-query", ex.Message); + Assert.Contains("Test failure reason", ex.Message); + } + + [Fact] + public async Task HandleControlSignal_SkipControlSignals_DoesNotPublish() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + SkipControlSignals = true + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + + // Act + await _handler.HandleControlSignal(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.PublishEventAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Never); + } + + [Fact] + public async Task HandleControlSignal_PackedFormat_PublishesPackedEvent() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed, + SkipControlSignals = false + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleControlSignal(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny() + ), Times.Once); + + _mockFailureTracker.Verify(ft => ft.ResetFailures("test-query"), Times.Once); + } + + [Fact] + public async Task HandleControlSignal_UnpackedFormat_PublishesUnpackedEvent() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Unpacked, + SkipControlSignals = false + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleControlSignal(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny() + ), Times.Once); + + _mockFailureTracker.Verify(ft => ft.ResetFailures("test-query"), Times.Once); + } + + [Fact] + public async Task HandleControlSignal_PublishFails_RecordsFailureAndRethrows() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed, + SkipControlSignals = false, + MaxFailureCount = 3 + }; + + var exception = new DaprException("Test error"); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .ThrowsAsync(exception); + + _mockFailureTracker.Setup(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny())) + .Returns(false); // Not yet failed + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _handler.HandleControlSignal(evt, config)); + Assert.Same(exception, ex); + + _mockFailureTracker.Verify(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny() + ), Times.Once); + } + + [Fact] + public async Task HandleControlSignal_MultipleFailuresExceedingThreshold_MarksQueryAsFailed() + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = ControlSignalKind.Running } + }; + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Packed, + SkipControlSignals = false, + MaxFailureCount = 3 + }; + + var exception = new DaprException("Test error"); + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .ThrowsAsync(exception); + + _mockFailureTracker.Setup(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny())) + .Returns(true); // Query is now failed + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.HandleControlSignal(evt, config)); + + _mockFailureTracker.Verify(ft => ft.RecordFailure( + "test-query", + config.MaxFailureCount, + It.IsAny() + ), Times.Once); + } + + [Fact] + public async Task HandleControlSignal_DifferentControlSignalTypes_FormatsCorrectly() + { + // Arrange and run test for each control signal type + foreach (ControlSignalKind signalKind in Enum.GetValues(typeof(ControlSignalKind))) + { + // Arrange + var evt = new ControlEvent { + QueryId = "test-query", + ControlSignal = new ControlSignalClass { Kind = signalKind } + }; + + var config = new QueryConfig { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + Packed = OutputFormat.Unpacked, // Test unpacked format + SkipControlSignals = false + }; + + _mockFailureTracker.Setup(ft => ft.IsQueryFailed("test-query")).Returns(false); + _mockDaprClient.Setup(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.IsAny(), + null, + It.IsAny())) + .Returns(Task.CompletedTask); + + // Reset call counts before each run + _mockDaprClient.Invocations.Clear(); + _mockFailureTracker.Invocations.Clear(); + + // Act + await _handler.HandleControlSignal(evt, config); + + // Assert + _mockDaprClient.Verify(dc => dc.InvokeBindingAsync( + config.BindingName, + config.BindingOperation, + It.Is(je => je.GetRawText().Contains($"\"kind\":\"{JsonNamingPolicy.CamelCase.ConvertName(signalKind.ToString())}\"")), + null, + It.IsAny() + ), Times.Once, $"Failed for control signal type: {signalKind}"); + } + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/DaprInitializationServiceTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/DaprInitializationServiceTests.cs new file mode 100644 index 00000000..1d516b33 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/DaprInitializationServiceTests.cs @@ -0,0 +1,110 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Dapr; +using Dapr.Client; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class DaprInitializationServiceTests +{ + private readonly Mock _mockDaprClient; + private readonly Mock> _mockLogger; + private readonly Mock _mockErrorStateHandler; + private readonly DaprInitializationService _service; + + public DaprInitializationServiceTests() + { + _mockDaprClient = new Mock(); + _mockLogger = new Mock>(); + _mockErrorStateHandler = new Mock(); + + _service = new DaprInitializationService( + _mockDaprClient.Object, + _mockLogger.Object, + _mockErrorStateHandler.Object + ); + } + + [Fact] + public async Task WaitForDaprSidecarAsync_Success_LogsInfoMessage() + { + // Arrange + _mockDaprClient.Setup(d => d.WaitForSidecarAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _service.WaitForDaprSidecarAsync(CancellationToken.None); + + // Assert + _mockDaprClient.Verify(d => d.WaitForSidecarAsync(It.IsAny()), Times.Once); + _mockErrorStateHandler.Verify(e => e.Terminate(It.IsAny()), Times.Never); + } + + [Fact] + public async Task WaitForDaprSidecarAsync_DaprException_TerminatesAndRethrows() + { + // Arrange + var exception = new DaprException("Dapr sidecar not available"); + _mockDaprClient.Setup(d => d.WaitForSidecarAsync(It.IsAny())) + .ThrowsAsync(exception); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + _service.WaitForDaprSidecarAsync(CancellationToken.None)); + + Assert.Same(exception, ex); + _mockErrorStateHandler.Verify(e => e.Terminate(It.Is(s => + s.Contains("Dapr sidecar is not available"))), Times.Once); + } + + [Fact] + public async Task WaitForDaprSidecarAsync_OtherException_TerminatesAndRethrows() + { + // Arrange + var exception = new Exception("Unexpected error"); + _mockDaprClient.Setup(d => d.WaitForSidecarAsync(It.IsAny())) + .ThrowsAsync(exception); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + _service.WaitForDaprSidecarAsync(CancellationToken.None)); + + Assert.Same(exception, ex); + _mockErrorStateHandler.Verify(e => e.Terminate(It.Is(s => + s.Contains("Unexpected error while waiting for Dapr sidecar"))), Times.Once); + } + + [Fact] + public async Task WaitForDaprSidecarAsync_Cancelled_DoesNotCallTerminate() + { + // Arrange + var cts = new CancellationTokenSource(); + cts.Cancel(); + var cancellationToken = cts.Token; + + _mockDaprClient.Setup(d => d.WaitForSidecarAsync(cancellationToken)) + .ThrowsAsync(new OperationCanceledException()); + + // Act & Assert + await Assert.ThrowsAsync(() => + _service.WaitForDaprSidecarAsync(cancellationToken)); + + _mockErrorStateHandler.Verify(e => e.Terminate(It.IsAny()), Times.Never); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/Drasi.Reactions.PostDaprOutputBinding.Tests.csproj b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/Drasi.Reactions.PostDaprOutputBinding.Tests.csproj new file mode 100644 index 00000000..0d19df6b --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/Drasi.Reactions.PostDaprOutputBinding.Tests.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ErrorStateHandlerTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ErrorStateHandlerTests.cs new file mode 100644 index 00000000..e04b09cf --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/ErrorStateHandlerTests.cs @@ -0,0 +1,50 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class ErrorStateHandlerTests +{ + [Fact] + public void Terminate_CallsTerminateWithError() + { + // This test would typically use reflection or a wrapper to test that the static method is called. + // Since we can't easily mock static methods in C#, this test is more of a placeholder. + // In a real-world scenario, we'd need to refactor to make the code more testable. + + // Arrange + var handler = new ErrorStateHandler(); +#pragma warning disable CS0219 // Variable is assigned but its value is never used + var errorMessage = "Test error message"; +#pragma warning restore CS0219 // Variable is assigned but its value is never used + + // We're not actually testing anything here since we can't easily verify the static method call + // without introducing additional complexity or using a mocking framework like Typemock Isolator. + + // This is more of a documentation to show the intent of the test. + // In a proper test setup, we'd verify that Reaction.TerminateWithError is called + // with the expected error message. + + // Act - in a real test, we'd catch an exception or use a test fixture + // handler.Terminate(errorMessage); + + // Assert - in a real test, we'd verify the static method was called + // Assert.True(...); + + // For now, we just assert that the class exists and doesn't throw when used + Assert.NotNull(handler); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigTests.cs new file mode 100644 index 00000000..c5836e3f --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigTests.cs @@ -0,0 +1,96 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel.DataAnnotations; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class QueryConfigTests +{ + // Would we have a default constructor for bindings? + + [Fact] + public void ValidateConfig_ValidConfiguration_ShouldHaveNoErrors() + { + // Arrange + var config = new QueryConfig + { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + MaxFailureCount = 10 + }; + + var validationContext = new ValidationContext(config); + var validationResults = new List(); + + // Act + var isValid = Validator.TryValidateObject(config, validationContext, validationResults, true); + + // Assert + Assert.True(isValid); + Assert.Empty(validationResults); + } + + [Theory] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + public void ValidateConfig_EmptyRequiredValue_ShouldHaveError(bool bindingNameEmpty, bool bindingTypeEmpty, + bool bindingOperationEmpty) + { + // Arrange + var config = new QueryConfig + { + BindingName = bindingNameEmpty ? "" : "test-", + BindingType = bindingTypeEmpty ? "" : "binding-type", + BindingOperation = bindingOperationEmpty ? "" : "exec", + }; + + var validationContext = new ValidationContext(config); + var validationResults = new List(); + + // Act + var isValid = Validator.TryValidateObject(config, validationContext, validationResults, true); + + // Assert + Assert.False(isValid); + Assert.Single(validationResults); + } + + [Fact] + public void ValidateConfig_NegativeMaxFailureCount_ShouldHaveError() + { + // Arrange + var config = new QueryConfig + { + BindingName = "test-binding", + BindingType = "binding-type", + BindingOperation = "exec", + MaxFailureCount = -1 + }; + + var validationContext = new ValidationContext(config); + var validationResults = new List(); + + // Act + var isValid = Validator.TryValidateObject(config, validationContext, validationResults, true); + + // Assert + Assert.False(isValid); + Assert.Single(validationResults); + Assert.Contains(validationResults, r => r.MemberNames.Contains("MaxFailureCount")); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigValidationServiceTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigValidationServiceTests.cs new file mode 100644 index 00000000..4c7060a0 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryConfigValidationServiceTests.cs @@ -0,0 +1,168 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Drasi.Reaction.SDK.Services; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class QueryConfigValidationServiceTests +{ + private readonly Mock> _mockLogger; + private readonly Mock _mockQueryConfigService; + private readonly Mock _mockErrorStateHandler; + private readonly QueryConfigValidationService _service; + + public QueryConfigValidationServiceTests() + { + _mockLogger = new Mock>(); + _mockQueryConfigService = new Mock(); + _mockErrorStateHandler = new Mock(); + + _service = new QueryConfigValidationService( + _mockLogger.Object, + _mockQueryConfigService.Object, + _mockErrorStateHandler.Object + ); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_NoQueries_LogsWarningAndSucceeds() + { + // Arrange + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(new List()); + + // Act + await _service.ValidateQueryConfigsAsync(CancellationToken.None); + + // Assert + _mockErrorStateHandler.Verify(e => e.Terminate(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_NullQueryConfig_TerminatesWithError() + { + // Arrange + var queryNames = new List { "test-query" }; + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(queryNames); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("test-query")).Returns((QueryConfig?)null); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + _service.ValidateQueryConfigsAsync(CancellationToken.None)); + + Assert.Contains("test-query", ex.Message); + _mockErrorStateHandler.Verify(e => e.Terminate(It.Is(s => s.Contains("test-query"))), Times.Once); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_InvalidConfig_TerminatesWithError() + { + // Arrange + var queryNames = new List { "test-query" }; + var invalidConfig = new QueryConfig { + BindingName = "", // Empty - invalid + BindingType = "binding-type", + BindingOperation = "exec" + }; + + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(queryNames); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("test-query")).Returns(invalidConfig); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + _service.ValidateQueryConfigsAsync(CancellationToken.None)); + + Assert.Contains("test-query", ex.Message); + Assert.Contains("BindingName", ex.Message); + _mockErrorStateHandler.Verify(e => e.Terminate(It.Is(s => + s.Contains("test-query") && s.Contains("BindingName"))), Times.Once); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_ValidConfig_Succeeds() + { + // Arrange + var queryNames = new List { "test-query" }; + var validConfig = new QueryConfig { + BindingName = "example-binding", + BindingType = "binding-type", + BindingOperation = "exec" + }; + + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(queryNames); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("test-query")).Returns(validConfig); + + // Act + await _service.ValidateQueryConfigsAsync(CancellationToken.None); + + // Assert + _mockErrorStateHandler.Verify(e => e.Terminate(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_MultipleQueries_ValidatesAll() + { + // Arrange + var queryNames = new List { "query1", "query2" }; + var validConfig1 = new QueryConfig { + BindingName = "example-binding-1", + BindingType = "binding-type", + BindingOperation = "exec" + }; + var validConfig2 = new QueryConfig { + BindingName = "example-binding-2", + BindingType = "binding-type", + BindingOperation = "exec" + }; + + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(queryNames); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("query1")).Returns(validConfig1); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("query2")).Returns(validConfig2); + + // Act + await _service.ValidateQueryConfigsAsync(CancellationToken.None); + + // Assert + _mockErrorStateHandler.Verify(e => e.Terminate(It.IsAny()), Times.Never); + _mockQueryConfigService.Verify(q => q.GetQueryConfig("query1"), Times.Once); + _mockQueryConfigService.Verify(q => q.GetQueryConfig("query2"), Times.Once); + } + + [Fact] + public async Task ValidateQueryConfigsAsync_FirstQueryInvalid_StopsValidationAndTerminates() + { + // Arrange + var queryNames = new List { "query1", "query2" }; + var invalidConfig = new QueryConfig { + BindingName = "", + BindingType = "binding-type", + BindingOperation = "exec" + }; + + _mockQueryConfigService.Setup(q => q.GetQueryNames()).Returns(queryNames); + _mockQueryConfigService.Setup(q => q.GetQueryConfig("query1")).Returns(invalidConfig); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + _service.ValidateQueryConfigsAsync(CancellationToken.None)); + + // Second query should not be checked after first fails + _mockQueryConfigService.Verify(q => q.GetQueryConfig("query2"), Times.Never); + _mockErrorStateHandler.Verify(e => e.Terminate(It.Is(s => s.Contains("query1"))), Times.Once); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryFailureTrackerTests.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryFailureTrackerTests.cs new file mode 100644 index 00000000..5ef53886 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding.Tests/QueryFailureTrackerTests.cs @@ -0,0 +1,185 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Drasi.Reactions.PostDaprOutputBinding.Tests; + +public class QueryFailureTrackerTests +{ + private readonly Mock> _mockLogger; + private readonly QueryFailureTracker _tracker; + + public QueryFailureTrackerTests() + { + _mockLogger = new Mock>(); + _tracker = new QueryFailureTracker(_mockLogger.Object); + } + + [Fact] + public void RecordFailure_BelowThreshold_ShouldNotMarkQueryAsFailed() + { + // Arrange + string queryId = "test-query"; + int maxFailures = 3; + + // Act + bool failed1 = _tracker.RecordFailure(queryId, maxFailures, "First failure"); + bool failed2 = _tracker.RecordFailure(queryId, maxFailures, "Second failure"); + + // Assert + Assert.False(failed1); + Assert.False(failed2); + Assert.False(_tracker.IsQueryFailed(queryId)); + Assert.Null(_tracker.GetFailureReason(queryId)); + } + + [Fact] + public void RecordFailure_ReachesThreshold_ShouldMarkQueryAsFailed() + { + // Arrange + string queryId = "test-query"; + int maxFailures = 3; + + // Act + _tracker.RecordFailure(queryId, maxFailures, "First failure"); + _tracker.RecordFailure(queryId, maxFailures, "Second failure"); + bool failed3 = _tracker.RecordFailure(queryId, maxFailures, "Third failure"); + + // Assert + Assert.True(failed3); + Assert.True(_tracker.IsQueryFailed(queryId)); + Assert.Contains("Third failure", _tracker.GetFailureReason(queryId) ?? string.Empty); + } + + [Fact] + public void RecordFailure_ExceedsThreshold_ShouldKeepQueryAsFailed() + { + // Arrange + string queryId = "test-query"; + int maxFailures = 2; + + // Act + _tracker.RecordFailure(queryId, maxFailures, "First failure"); + bool failed2 = _tracker.RecordFailure(queryId, maxFailures, "Second failure"); + bool failed3 = _tracker.RecordFailure(queryId, maxFailures, "Third failure"); + + // Assert + Assert.True(failed2); // Second failure reaches threshold + Assert.True(failed3); // Third failure, already failed + Assert.True(_tracker.IsQueryFailed(queryId)); + Assert.Contains("Second failure", _tracker.GetFailureReason(queryId) ?? string.Empty); + } + + [Fact] + public void RecordFailure_ThresholdOfOne_ShouldMarkQueryAsFailedImmediately() + { + // Arrange + string queryId = "test-query"; + int maxFailures = 1; + + // Act + bool failed = _tracker.RecordFailure(queryId, maxFailures, "Only failure"); + + // Assert + Assert.True(failed); + Assert.True(_tracker.IsQueryFailed(queryId)); + Assert.Contains("Only failure", _tracker.GetFailureReason(queryId) ?? string.Empty); + } + + [Fact] + public void ResetFailures_ShouldResetFailedState() + { + // Arrange + string queryId = "test-query"; + int maxFailures = 1; + _tracker.RecordFailure(queryId, maxFailures, "Only failure"); + Assert.True(_tracker.IsQueryFailed(queryId)); // Verify setup + + // Act + _tracker.ResetFailures(queryId); + + // Assert + Assert.False(_tracker.IsQueryFailed(queryId)); + Assert.Null(_tracker.GetFailureReason(queryId)); + } + + [Fact] + public void IsQueryFailed_NonExistentQuery_ShouldReturnFalse() + { + // Act & Assert + Assert.False(_tracker.IsQueryFailed("non-existent-query")); + } + + [Fact] + public void GetFailureReason_NonExistentQuery_ShouldReturnNull() + { + // Act & Assert + Assert.Null(_tracker.GetFailureReason("non-existent-query")); + } + + [Fact] + public void ResetFailures_NonExistentQuery_ShouldNotThrowException() + { + // Act & Assert - should not throw + _tracker.ResetFailures("non-existent-query"); + } + + [Fact] + public void MultipleQueries_ShouldTrackIndependently() + { + // Arrange + string query1 = "query1"; + string query2 = "query2"; + int maxFailures = 2; + + // Act + _tracker.RecordFailure(query1, maxFailures, "Query1 failure 1"); + _tracker.RecordFailure(query2, maxFailures, "Query2 failure 1"); + bool query1Failed = _tracker.RecordFailure(query1, maxFailures, "Query1 failure 2"); + + // Assert + Assert.True(query1Failed); + Assert.True(_tracker.IsQueryFailed(query1)); + Assert.False(_tracker.IsQueryFailed(query2)); + + // Reset only query1 + _tracker.ResetFailures(query1); + Assert.False(_tracker.IsQueryFailed(query1)); + Assert.False(_tracker.IsQueryFailed(query2)); + } + + [Fact] + public async Task RecordFailure_ConcurrentOperations_ShouldBeThreadSafe() + { + // Arrange + const string queryId = "test-query"; + const int maxFailures = 100; + var tasks = new List>(); + + // Act + for (var i = 0; i < 200; i++) + { + var i1 = i; + tasks.Add(Task.Run(() => _tracker.RecordFailure(queryId, maxFailures, $"Failure {i1}"))); + } + + await Task.WhenAll(tasks.ToArray()); + + // Assert + Assert.True(_tracker.IsQueryFailed(queryId)); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj new file mode 100644 index 00000000..0f14dbdd --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Drasi.Reactions.PostDaprOutputBinding.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/ErrorStateHandler.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/ErrorStateHandler.cs new file mode 100644 index 00000000..bd9cc115 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/ErrorStateHandler.cs @@ -0,0 +1,39 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Drasi.Reactions.PostDaprOutputBinding; + +/// +/// Interface for handling terminal error states. +/// +public interface IErrorStateHandler +{ + /// + /// Terminate the reaction with an error message. + /// + /// Error message explaining the termination reason + void Terminate(string message); +} + +/// +/// Implementation of the error state handler. +/// +public class ErrorStateHandler : IErrorStateHandler +{ + /// + public void Terminate(string message) + { + Reaction.SDK.Reaction.TerminateWithError(message); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Models/Unpacked.generated.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Models/Unpacked.generated.cs new file mode 100644 index 00000000..5d8eec8d --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Models/Unpacked.generated.cs @@ -0,0 +1,603 @@ +// +// +// To parse this JSON data, add NuGet 'System.Text.Json' then do one of these: +// +// using Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked; +// +// var changeNotification = ChangeNotification.FromJson(jsonString); +// var changePayload = ChangePayload.FromJson(jsonString); +// var changeSource = ChangeSource.FromJson(jsonString); +// var controlPayload = ControlPayload.FromJson(jsonString); +// var controlSignalNotification = ControlSignalNotification.FromJson(jsonString); +// var notification = Notification.FromJson(jsonString); +// var op = Op.FromJson(jsonString); +// var reloadHeader = ReloadHeader.FromJson(jsonString); +// var reloadItem = ReloadItem.FromJson(jsonString); +// var versions = Versions.FromJson(jsonString); +#nullable enable +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class ChangeNotification + { + [JsonPropertyName("op")] + public ChangeNotificationOp Op { get; set; } + + [JsonPropertyName("payload")] + public PayloadClass Payload { get; set; } + + /// + /// The sequence number of the source change + /// + [JsonPropertyName("seq")] + public long Seq { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class PayloadClass + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("after")] + public Dictionary After { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("before")] + public Dictionary Before { get; set; } + + [JsonPropertyName("source")] + public SourceClass Source { get; set; } + } + + public partial class SourceClass + { + /// + /// The ID of the query that the change originated from + /// + [JsonPropertyName("queryId")] + public string QueryId { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class ChangePayload + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("after")] + public Dictionary After { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("before")] + public Dictionary Before { get; set; } + + [JsonPropertyName("source")] + public SourceClass Source { get; set; } + } + + public partial class ChangeSource + { + /// + /// The ID of the query that the change originated from + /// + [JsonPropertyName("queryId")] + public string QueryId { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class ControlPayload + { + [JsonPropertyName("kind")] + public string Kind { get; set; } + + [JsonPropertyName("source")] + public SourceClass Source { get; set; } + } + + public partial class ControlSignalNotification + { + [JsonPropertyName("op")] + public ControlSignalNotificationOp Op { get; set; } + + [JsonPropertyName("payload")] + public ControlSignalNotificationPayload Payload { get; set; } + + /// + /// The sequence number of the control signal + /// + [JsonPropertyName("seq")] + public long Seq { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class ControlSignalNotificationPayload + { + [JsonPropertyName("kind")] + public string Kind { get; set; } + + [JsonPropertyName("source")] + public SourceClass Source { get; set; } + } + + public partial class Notification + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonPropertyName("op")] + public OpEnum Op { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class ReloadHeader + { + [JsonPropertyName("op")] + public ReloadHeaderOp Op { get; set; } + + /// + /// The sequence number of last known source change + /// + [JsonPropertyName("seq")] + public long Seq { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public partial class ReloadItem + { + [JsonPropertyName("op")] + public ReloadItemOp Op { get; set; } + + [JsonPropertyName("payload")] + public PayloadClass Payload { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonPropertyName("ts_ms")] + public long TsMs { get; set; } + } + + public enum ChangeNotificationOp { D, I, U }; + + public enum ControlSignalNotificationOp { X }; + + public enum OpEnum { D, H, I, R, U, X }; + + public enum ReloadHeaderOp { H }; + + public enum ReloadItemOp { R }; + + public enum VersionsEnum { V1 }; + + public partial class ChangeNotification + { + public static ChangeNotification FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ChangePayload + { + public static ChangePayload FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ChangeSource + { + public static ChangeSource FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ControlPayload + { + public static ControlPayload FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ControlSignalNotification + { + public static ControlSignalNotification FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class Notification + { + public static Notification FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public class Op + { + public static OpEnum FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ReloadHeader + { + public static ReloadHeader FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public partial class ReloadItem + { + public static ReloadItem FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public class Versions + { + public static VersionsEnum FromJson(string json) => JsonSerializer.Deserialize(json, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this ChangeNotification self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ChangePayload self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ChangeSource self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ControlPayload self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ControlSignalNotification self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this Notification self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this OpEnum self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ReloadHeader self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this ReloadItem self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + public static string ToJson(this VersionsEnum self) => JsonSerializer.Serialize(self, Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + ChangeNotificationOpConverter.Singleton, + ControlSignalNotificationOpConverter.Singleton, + OpEnumConverter.Singleton, + ReloadHeaderOpConverter.Singleton, + ReloadItemOpConverter.Singleton, + VersionsEnumConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; + } + + internal class ChangeNotificationOpConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ChangeNotificationOp); + + public override ChangeNotificationOp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "d": + return ChangeNotificationOp.D; + case "i": + return ChangeNotificationOp.I; + case "u": + return ChangeNotificationOp.U; + } + throw new Exception("Cannot unmarshal type ChangeNotificationOp"); + } + + public override void Write(Utf8JsonWriter writer, ChangeNotificationOp value, JsonSerializerOptions options) + { + switch (value) + { + case ChangeNotificationOp.D: + JsonSerializer.Serialize(writer, "d", options); + return; + case ChangeNotificationOp.I: + JsonSerializer.Serialize(writer, "i", options); + return; + case ChangeNotificationOp.U: + JsonSerializer.Serialize(writer, "u", options); + return; + } + throw new Exception("Cannot marshal type ChangeNotificationOp"); + } + + public static readonly ChangeNotificationOpConverter Singleton = new ChangeNotificationOpConverter(); + } + + internal class ControlSignalNotificationOpConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ControlSignalNotificationOp); + + public override ControlSignalNotificationOp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + if (value == "x") + { + return ControlSignalNotificationOp.X; + } + throw new Exception("Cannot unmarshal type ControlSignalNotificationOp"); + } + + public override void Write(Utf8JsonWriter writer, ControlSignalNotificationOp value, JsonSerializerOptions options) + { + if (value == ControlSignalNotificationOp.X) + { + JsonSerializer.Serialize(writer, "x", options); + return; + } + throw new Exception("Cannot marshal type ControlSignalNotificationOp"); + } + + public static readonly ControlSignalNotificationOpConverter Singleton = new ControlSignalNotificationOpConverter(); + } + + internal class OpEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(OpEnum); + + public override OpEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "d": + return OpEnum.D; + case "h": + return OpEnum.H; + case "i": + return OpEnum.I; + case "r": + return OpEnum.R; + case "u": + return OpEnum.U; + case "x": + return OpEnum.X; + } + throw new Exception("Cannot unmarshal type OpEnum"); + } + + public override void Write(Utf8JsonWriter writer, OpEnum value, JsonSerializerOptions options) + { + switch (value) + { + case OpEnum.D: + JsonSerializer.Serialize(writer, "d", options); + return; + case OpEnum.H: + JsonSerializer.Serialize(writer, "h", options); + return; + case OpEnum.I: + JsonSerializer.Serialize(writer, "i", options); + return; + case OpEnum.R: + JsonSerializer.Serialize(writer, "r", options); + return; + case OpEnum.U: + JsonSerializer.Serialize(writer, "u", options); + return; + case OpEnum.X: + JsonSerializer.Serialize(writer, "x", options); + return; + } + throw new Exception("Cannot marshal type OpEnum"); + } + + public static readonly OpEnumConverter Singleton = new OpEnumConverter(); + } + + internal class ReloadHeaderOpConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ReloadHeaderOp); + + public override ReloadHeaderOp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + if (value == "h") + { + return ReloadHeaderOp.H; + } + throw new Exception("Cannot unmarshal type ReloadHeaderOp"); + } + + public override void Write(Utf8JsonWriter writer, ReloadHeaderOp value, JsonSerializerOptions options) + { + if (value == ReloadHeaderOp.H) + { + JsonSerializer.Serialize(writer, "h", options); + return; + } + throw new Exception("Cannot marshal type ReloadHeaderOp"); + } + + public static readonly ReloadHeaderOpConverter Singleton = new ReloadHeaderOpConverter(); + } + + internal class ReloadItemOpConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ReloadItemOp); + + public override ReloadItemOp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + if (value == "r") + { + return ReloadItemOp.R; + } + throw new Exception("Cannot unmarshal type ReloadItemOp"); + } + + public override void Write(Utf8JsonWriter writer, ReloadItemOp value, JsonSerializerOptions options) + { + if (value == ReloadItemOp.R) + { + JsonSerializer.Serialize(writer, "r", options); + return; + } + throw new Exception("Cannot marshal type ReloadItemOp"); + } + + public static readonly ReloadItemOpConverter Singleton = new ReloadItemOpConverter(); + } + + internal class VersionsEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(VersionsEnum); + + public override VersionsEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + if (value == "v1") + { + return VersionsEnum.V1; + } + throw new Exception("Cannot unmarshal type VersionsEnum"); + } + + public override void Write(Utf8JsonWriter writer, VersionsEnum value, JsonSerializerOptions options) + { + if (value == VersionsEnum.V1) + { + JsonSerializer.Serialize(writer, "v1", options); + return; + } + throw new Exception("Cannot marshal type VersionsEnum"); + } + + public static readonly VersionsEnumConverter Singleton = new VersionsEnumConverter(); + } + + public class DateOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + public class TimeOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + internal class IsoDateTimeOffsetConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); + } +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Program.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Program.cs new file mode 100644 index 00000000..0a3c5c88 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Program.cs @@ -0,0 +1,63 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Drasi.Reaction.SDK; +using Drasi.Reactions.PostDaprOutputBinding; +using Drasi.Reactions.PostDaprOutputBinding.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +try +{ + var reaction = new ReactionBuilder() + .UseChangeEventHandler() + .UseControlEventHandler() + .UseJsonQueryConfig() + .ConfigureServices(services => + { + // Register formatters + services.AddSingleton(); + services.AddSingleton(); + + // Register services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register Dapr client + services.AddDaprClient(); + }) + .Build(); + + var logger = reaction.Services.GetRequiredService>(); + logger.LogInformation("Starting PostDaprOutputBinding reaction"); + + // Step 1. Wait for Dapr sidecar + var daprInitService = reaction.Services.GetRequiredService(); + await daprInitService.WaitForDaprSidecarAsync(CancellationToken.None); + + // Step 2. Validate query configurations + var validationService = reaction.Services.GetRequiredService(); + await validationService.ValidateQueryConfigsAsync(CancellationToken.None); + + // Step 3. Start the reaction + await reaction.StartAsync(); +} +catch (Exception ex) +{ + var errorStateHandler = new ErrorStateHandler(); + errorStateHandler.Terminate($"Fatal error starting PostDaprOutputBinding reaction: {ex.Message}"); + throw; +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryConfig.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryConfig.cs new file mode 100644 index 00000000..e59578f6 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryConfig.cs @@ -0,0 +1,65 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Drasi.Reactions.PostDaprOutputBinding; + +/// +/// Configuration for the PostDaprOutputBinding reaction. +/// Maps Drasi queries to Dapr output bindings. +/// +public class QueryConfig : IValidatableObject +{ + /// + /// Name of the Dapr output binding component to use for publishing. + /// + [Required] + [JsonPropertyName("bindingName")] + public string BindingName { get; set; } = "drasi-output-binding"; + + [Required] + [JsonPropertyName("bindingOperation")] + public required string BindingOperation { get; set; } + + [JsonPropertyName("bindingMetadataTemplate")] + public Dictionary? BindingMetadataTemplate { get; set; } + + [Required] + [JsonPropertyName("bindingType")] + public required string BindingType { get; set; } + + public string SecretUserName { get; set; } = string.Empty; + public string Secret { get; set; } = string.Empty; + + /// + /// Whether to pack the events into a single message (true) or send as individual messages (false). + /// + [JsonPropertyName("packed")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public OutputFormat Packed { get; set; } = OutputFormat.Unpacked; + + /// + /// Maximum consecutive failures before marking query as failed. + /// + [JsonPropertyName("maxFailureCount")] + public int MaxFailureCount { get; set; } = 5; + + /// + /// Whether to skip publishing control signals to the topic. + /// + [JsonPropertyName("skipControlSignals")] + public bool SkipControlSignals { get; set; } = false; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (MaxFailureCount <= 0) + { + yield return new ValidationResult("MaxFailureCount must be greater than 0", [nameof(MaxFailureCount)]); + } + } +} + +public enum OutputFormat +{ + Packed = 1, + Unpacked = 0 +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryFailureTracker.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryFailureTracker.cs new file mode 100644 index 00000000..7e53e3dd --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/QueryFailureTracker.cs @@ -0,0 +1,160 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace Drasi.Reactions.PostDaprOutputBinding; + +/// +/// Interface for tracking query failures. +/// +public interface IQueryFailureTracker +{ + /// + /// Records a failure for the specified query. + /// + /// The query ID + /// Maximum allowed failures before query is considered failed + /// Reason for the failure + /// True if the query is now in a failed state + bool RecordFailure(string queryId, int maxFailureCount, string reason); + + /// + /// Reset the failure count for a query. + /// + /// The query ID + void ResetFailures(string queryId); + + /// + /// Checks if a query is in a failed state. + /// + /// The query ID + /// True if query is in failed state + bool IsQueryFailed(string queryId); + + /// + /// Gets the reason a query failed. + /// + /// The query ID + /// The failure reason or null if not failed + string? GetFailureReason(string queryId); +} + +/// +/// Class to store the state of a query in the failure tracker. +/// +public class QueryState +{ + /// + /// Current count of consecutive failures. + /// + public int FailureCount { get; set; } = 0; + + /// + /// Whether the query is in a failed state. + /// + public bool IsFailed { get; set; } = false; + + /// + /// The reason for failure, if the query is in a failed state. + /// + public string? Reason { get; set; } +} + +/// +/// Implementation of the query failure tracker using ConcurrentDictionary. +/// +public class QueryFailureTracker : IQueryFailureTracker +{ + private readonly ConcurrentDictionary _queryStates = new(); + private readonly ILogger _logger; + + public QueryFailureTracker(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public bool RecordFailure(string queryId, int maxFailureCount, string reason) + { + var result = _queryStates.AddOrUpdate( + queryId, + // If the query doesn't exist yet, create a new state with failure count 1 + _ => + { + var isFailed = 1 >= maxFailureCount; + var newState = new QueryState + { + FailureCount = 1, + IsFailed = isFailed, + Reason = isFailed + ? $"Exceeded maximum failure count ({maxFailureCount}): {reason}" + : null + }; + + _logger.LogWarning("Query {QueryId} failure count: 1/{MaxFailures}", + queryId, maxFailureCount); + + if (isFailed) + { + _logger.LogError("Query {QueryId} marked as failed: {Reason}", + queryId, newState.Reason); + } + + return newState; + }, + // If the query already exists, update its state + (_, state) => + { + // Only increment and check if not already in failed state + if (!state.IsFailed) + { + state.FailureCount++; + + _logger.LogWarning("Query {QueryId} failure count: {FailureCount}/{MaxFailures}", + queryId, state.FailureCount, maxFailureCount); + + if (state.FailureCount >= maxFailureCount) + { + state.IsFailed = true; + state.Reason = $"Exceeded maximum failure count ({maxFailureCount}): {reason}"; + + _logger.LogError("Query {QueryId} marked as failed: {Reason}", + queryId, state.Reason); + } + } + return state; + }); + + return result.IsFailed; + } + + public void ResetFailures(string queryId) + { + _queryStates.AddOrUpdate( + queryId, + _ => new QueryState(), + (_, _) => new QueryState()); + } + + public bool IsQueryFailed(string queryId) + { + return _queryStates.TryGetValue(queryId, out var state) && state.IsFailed; + } + + public string? GetFailureReason(string queryId) + { + return _queryStates.TryGetValue(queryId, out var state) ? state.Reason : null; + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeFormatterFactory.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeFormatterFactory.cs new file mode 100644 index 00000000..e92da10c --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeFormatterFactory.cs @@ -0,0 +1,47 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.DependencyInjection; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +/// +/// Factory for creating appropriate change formatters based on the event format. +/// +public interface IChangeFormatterFactory +{ + /// + /// Gets a formatter for the specified event format. + /// + /// An appropriate change formatter + IChangeFormatter GetFormatter(); +} + +/// +/// Implementation of the change formatter factory. +/// +public class ChangeFormatterFactory : IChangeFormatterFactory +{ + private readonly IServiceProvider _serviceProvider; + + public ChangeFormatterFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + public IChangeFormatter GetFormatter() + { + return _serviceProvider.GetRequiredService(); + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeHandler.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeHandler.cs new file mode 100644 index 00000000..73a238e4 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ChangeHandler.cs @@ -0,0 +1,205 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Dynamic; +using System.Text.Json; +using Dapr.Client; +using Drasi.Reaction.SDK; +using Drasi.Reaction.SDK.Models.QueryOutput; +using Grpc.Core; +using HandlebarsDotNet; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +public class ChangeHandler( + DaprClient daprClient, + IChangeFormatterFactory formatterFactory, + ILogger logger, + IQueryFailureTracker failureTracker) + : IChangeEventHandler +{ + private readonly DaprClient _daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); + private readonly IChangeFormatterFactory _formatterFactory = formatterFactory ?? throw new ArgumentNullException(nameof(formatterFactory)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IQueryFailureTracker _failureTracker = failureTracker ?? throw new ArgumentNullException(nameof(failureTracker)); + + public async Task HandleChange(ChangeEvent evt, QueryConfig? config) + { + var queryId = evt.QueryId; + var queryConfig = config + ?? throw new ArgumentNullException(nameof(config), $"Query configuration is null for query {queryId}"); + + // Check if the query is already in a failed state + if (_failureTracker.IsQueryFailed(queryId)) + { + var reason = _failureTracker.GetFailureReason(queryId); + _logger.LogError("Rejecting change event for failed query {QueryId}. Reason: {Reason}", queryId, reason); + throw new InvalidOperationException($"Query {queryId} is in a failed state: {reason}"); + } + + _logger.LogDebug("Processing change event for query {QueryId} with binding {BindingName} of type {BindingType}", + queryId, queryConfig.BindingName, queryConfig.BindingType); + + try + { + if (queryConfig.Packed == OutputFormat.Packed) + { + // Send the complete change event as a single message + await PublishPackedEvent(evt, queryConfig); + } + else + { + // Format and send individual events for each change + await PublishUnpackedEvents(evt, queryConfig); + } + + // Reset failure count on success + _failureTracker.ResetFailures(queryId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error publishing event for query {QueryId}", queryId); + + // Track failure and check if query should be marked as failed + var exceptionMessage = $"{ex.Message} {ex.InnerException?.Message}"; + // Serialize the exception message for logging + //var exJson = JsonSerializer.Serialize(ex, ModelOptions.JsonOptions); + //_logger.LogError("Exception details: {ExceptionJson}", exJson); + bool isFailed = _failureTracker.RecordFailure( + queryId, + queryConfig.MaxFailureCount, + $"Error publishing to Dapr output binding: {exceptionMessage}"); + + if (isFailed) + { + _logger.LogError("Query {QueryId} has been marked as failed after {MaxFailureCount} consecutive failures", + queryId, queryConfig.MaxFailureCount); + } + + throw; // Rethrow to let Drasi SDK handle the failure + } + } + + private Dictionary? RenderMetadata(string source, object? result) + { + if (string.IsNullOrWhiteSpace(source) || result is null) + { + _logger.LogDebug("Metadata template is empty or null, skipping rendering."); + return null; + } + var template = Handlebars.Compile(source); + var rendered = template(result); + try + { + return JsonSerializer.Deserialize>(rendered, ModelOptions.JsonOptions) + ?? null; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize metadata from rendered template: {Rendered}", rendered); + throw; + } + } + + + private async Task PublishPackedEvent(ChangeEvent evt, QueryConfig queryConfig) + { + var serializedEvent = JsonSerializer.Serialize(evt, ModelOptions.JsonOptions); + using var doc = JsonDocument.Parse(serializedEvent); + await _daprClient.InvokeBindingAsync(bindingName: queryConfig.BindingName, operation: queryConfig.BindingOperation, + data: doc.RootElement); + _logger.LogDebug("Published packed event for query {QueryId}", evt.QueryId); + } + + private async Task PublishUnpackedEvents(ChangeEvent evt, QueryConfig queryConfig) + { + var formatter = _formatterFactory.GetFormatter(); + var events = formatter.Format(evt); + + var jsonElements = events as JsonElement[] ?? events.ToArray(); + foreach (var eventData in jsonElements) + { + + var metadata = RenderMetadata(JsonSerializer.Serialize(queryConfig.BindingMetadataTemplate), ConvertValue(eventData)); + try + { + await _daprClient.InvokeBindingAsync(bindingName: queryConfig.BindingName, + operation: queryConfig.BindingOperation, + data: eventData, metadata: metadata); + } + catch (Exception e) + { + _logger.LogError("Failed to publish event for query {QueryId} with error: {ErrorMessage}", + evt.QueryId, e.Message); + _logger.LogError("Event data: {EventData}", JsonSerializer.Serialize(eventData)); + } + } + + _logger.LogDebug("Published {Count} unpacked events for query {QueryId}", + jsonElements.Length, evt.QueryId); + } + + private static ExpandoObject ToExpando(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new ArgumentException("Root element must be an object", nameof(element)); + + IDictionary expando = new ExpandoObject(); + + foreach (var prop in element.EnumerateObject()) + { + expando[prop.Name] = ConvertValue(prop.Value); + } + + return (ExpandoObject)expando; + } + + private static object? ConvertValue(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return ToExpando(element); + + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + list.Add(ConvertValue(item)); + return list; + + case JsonValueKind.String: + return element.GetString(); + + case JsonValueKind.Number: + if (element.TryGetInt64(out var l)) return l; + if (element.TryGetDouble(out var d)) return d; + return element.GetDecimal(); + + case JsonValueKind.True: + case JsonValueKind.False: + return element.GetBoolean(); + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + return null; + + default: + return element.GetRawText(); + } + } + + +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ControlSignalHandler.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ControlSignalHandler.cs new file mode 100644 index 00000000..ab95122c --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/ControlSignalHandler.cs @@ -0,0 +1,124 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text.Json; +using Dapr.Client; +using Drasi.Reaction.SDK; +using Drasi.Reaction.SDK.Models.QueryOutput; +using Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked; +using Microsoft.Extensions.Logging; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +public class ControlSignalHandler : IControlEventHandler +{ + private readonly DaprClient _daprClient; + private readonly ILogger _logger; + private readonly IQueryFailureTracker _failureTracker; + + public ControlSignalHandler( + DaprClient daprClient, + ILogger logger, + IQueryFailureTracker failureTracker) + { + _daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _failureTracker = failureTracker ?? throw new ArgumentNullException(nameof(failureTracker)); + } + + public async Task HandleControlSignal(ControlEvent evt, QueryConfig? config) + { + var queryId = evt.QueryId; + var queryConfig = config + ?? throw new ArgumentNullException(nameof(config), $"Query configuration is null for query {queryId}"); + + // Check if the query is already in a failed state + if (_failureTracker.IsQueryFailed(queryId)) + { + var reason = _failureTracker.GetFailureReason(queryId); + _logger.LogError("Rejecting control signal for failed query {QueryId}. Reason: {Reason}", queryId, reason); + throw new InvalidOperationException($"Query {queryId} is in a failed state: {reason}"); + } + + if (queryConfig.SkipControlSignals) + { + _logger.LogDebug("Skipping control signal {SignalType} for query {QueryId} (skipControlSignals=true)", + evt.ControlSignal.Kind, queryId); + return; + } + + _logger.LogDebug("Processing control signal {SignalType} for query {QueryId} with binding {BindingName} of type {BindingType} with {Operation}", + evt.ControlSignal.Kind, queryId, queryConfig.BindingName, queryConfig.BindingType, queryConfig.BindingOperation); + + try + { + if (queryConfig.Packed == OutputFormat.Packed) + { + // Send the complete control event as a single message + var serializedEvent = JsonSerializer.Serialize(evt, ModelOptions.JsonOptions); + using var doc = JsonDocument.Parse(serializedEvent); + await _daprClient.InvokeBindingAsync(bindingName: queryConfig.BindingName, operation: queryConfig.BindingOperation, + data: doc.RootElement); + _logger.LogDebug("Published packed control signal for query {QueryId}", queryId); + } + else + { + // Create and send an unpacked control signal + var notification = new ControlSignalNotification + { + Op = ControlSignalNotificationOp.X, + TsMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Payload = new ControlSignalNotificationPayload() + { + Kind = JsonSerializer.Serialize(evt.ControlSignal.Kind, ModelOptions.JsonOptions).Trim('"'), + Source = new SourceClass() + { + QueryId = queryId, + TsMs = evt.SourceTimeMs + } + } + }; + + var serializedData = JsonSerializer.Serialize(notification, Converter.Settings); + using var doc = JsonDocument.Parse(serializedData); + var serializedEvent = doc.RootElement.Clone(); + + await _daprClient.InvokeBindingAsync(bindingName: queryConfig.BindingName, operation: queryConfig.BindingOperation, + data: serializedEvent); + _logger.LogDebug("Published unpacked control signal for query {QueryId}", queryId); + } + + // Reset failure count on success + _failureTracker.ResetFailures(queryId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error publishing control signal for query {QueryId}", queryId); + + // Track failure and check if query should be marked as failed + bool isFailed = _failureTracker.RecordFailure( + queryId, + queryConfig.MaxFailureCount, + $"Error publishing control signal to Dapr output binding: {ex.Message}"); + + if (isFailed) + { + _logger.LogError("Query {QueryId} has been marked as failed after {MaxFailureCount} consecutive failures", + queryId, queryConfig.MaxFailureCount); + } + + throw; // Rethrow to let Drasi SDK handle the failure + } + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DaprInitializationService.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DaprInitializationService.cs new file mode 100644 index 00000000..17bbf315 --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DaprInitializationService.cs @@ -0,0 +1,70 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Dapr; +using Dapr.Client; +using Microsoft.Extensions.Logging; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +public interface IDaprInitializationService +{ + Task WaitForDaprSidecarAsync(CancellationToken cancellationToken); +} + +public class DaprInitializationService : IDaprInitializationService +{ + private readonly DaprClient _daprClient; + private readonly ILogger _logger; + private readonly IErrorStateHandler _errorStateHandler; + + public DaprInitializationService( + DaprClient daprClient, + ILogger logger, + IErrorStateHandler errorStateHandler) + { + _daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _errorStateHandler = errorStateHandler ?? throw new ArgumentNullException(nameof(errorStateHandler)); + } + + public async Task WaitForDaprSidecarAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Waiting for Dapr sidecar to be available..."); + try + { + await _daprClient.WaitForSidecarAsync(cancellationToken); + _logger.LogInformation("Dapr sidecar is available."); + } + catch (OperationCanceledException) + { + _logger.LogWarning("Waiting for Dapr sidecar was canceled."); + throw; // Rethrow to allow the caller to handle cancellation + } + catch (DaprException ex) + { + var errorMessage = "Dapr sidecar is not available."; + _logger.LogError(ex, errorMessage); + _errorStateHandler.Terminate(errorMessage); + throw; + } + catch (Exception ex) + { + var errorMessage = "Unexpected error while waiting for Dapr sidecar."; + _logger.LogError(ex, errorMessage); + _errorStateHandler.Terminate(errorMessage); + throw; + } + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DrasiChangeFormatter.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DrasiChangeFormatter.cs new file mode 100644 index 00000000..a4900bdf --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/DrasiChangeFormatter.cs @@ -0,0 +1,101 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text.Json; +using Drasi.Reaction.SDK.Models.QueryOutput; +using Drasi.Reactions.PostDaprOutputBinding.Models.Unpacked; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +/// +/// Formatter for Drasi native format. +/// +public class DrasiChangeFormatter : IChangeFormatter +{ + public IEnumerable Format(ChangeEvent evt) + { + var notificationList = new List(); + foreach (var inputItem in evt.AddedResults) + { + var outputItem = new ChangeNotification + { + Op = ChangeNotificationOp.I, + TsMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Payload = new PayloadClass() + { + Source = new SourceClass() + { + QueryId = evt.QueryId, + TsMs = evt.SourceTimeMs + }, + After = inputItem + } + }; + notificationList.Add(outputItem); + } + + foreach (var inputItem in evt.UpdatedResults) + { + var outputItem = new ChangeNotification + { + Op = ChangeNotificationOp.U, + TsMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Payload = new PayloadClass() + { + Source = new SourceClass() + { + QueryId = evt.QueryId, + TsMs = evt.SourceTimeMs + }, + Before = inputItem.Before, + After = inputItem.After + } + }; + notificationList.Add(outputItem); + } + + foreach (var inputItem in evt.DeletedResults) + { + var outputItem = new ChangeNotification + { + Op = ChangeNotificationOp.D, + TsMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Payload = new PayloadClass() + { + Source = new SourceClass() + { + QueryId = evt.QueryId, + TsMs = evt.SourceTimeMs + }, + Before = inputItem + } + }; + notificationList.Add(outputItem); + } + + var result = new List(); + foreach (var item in notificationList) + { + var serializedDataJson = JsonSerializer.Serialize( + item, + Converter.Settings + ); + + using var doc = JsonDocument.Parse(serializedDataJson); + JsonElement serializedEvent = doc.RootElement.Clone(); + result.Add(serializedEvent); + } + return result; + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/IChangeFormatter.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/IChangeFormatter.cs new file mode 100644 index 00000000..54cb969e --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/IChangeFormatter.cs @@ -0,0 +1,31 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text.Json; +using Drasi.Reaction.SDK.Models.QueryOutput; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +/// +/// Interface for formatting change events. +/// +public interface IChangeFormatter +{ + /// + /// Formats a change event into a collection of JSON elements. + /// + /// The change event to format + /// A collection of formatted JSON elements + IEnumerable Format(ChangeEvent evt); +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/QueryConfigValidationService.cs b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/QueryConfigValidationService.cs new file mode 100644 index 00000000..e229a80f --- /dev/null +++ b/reactions/dapr/post-output-binding/Drasi.Reactions.PostDaprOutputBinding/Services/QueryConfigValidationService.cs @@ -0,0 +1,87 @@ +// Copyright 2024 The Drasi Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel.DataAnnotations; +using System.Text; +using Drasi.Reaction.SDK.Services; +using Microsoft.Extensions.Logging; + +namespace Drasi.Reactions.PostDaprOutputBinding.Services; + +public interface IQueryConfigValidationService +{ + Task ValidateQueryConfigsAsync(CancellationToken cancellationToken); +} + +public class QueryConfigValidationService : IQueryConfigValidationService +{ + private readonly ILogger _logger; + private readonly IQueryConfigService _queryConfigService; + private readonly IErrorStateHandler _errorStateHandler; + + public QueryConfigValidationService( + ILogger logger, + IQueryConfigService queryConfigService, + IErrorStateHandler errorStateHandler) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _queryConfigService = queryConfigService ?? throw new ArgumentNullException(nameof(queryConfigService)); + _errorStateHandler = errorStateHandler ?? throw new ArgumentNullException(nameof(errorStateHandler)); + } + + public Task ValidateQueryConfigsAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Validating query configurations..."); + + var queryNames = _queryConfigService.GetQueryNames(); + if (!queryNames.Any()) + { + _logger.LogWarning("No queries configured."); + return Task.CompletedTask; + } + + foreach (var queryName in queryNames) + { + QueryConfig? queryConfig; + queryConfig = _queryConfigService.GetQueryConfig(queryName); + if (queryConfig == null) + { + var errorMessage = $"Query configuration for '{queryName}' is null."; + _logger.LogError(errorMessage); + _errorStateHandler.Terminate(errorMessage); + throw new InvalidProgramException(errorMessage); + } + + var validationResults = new List(); + if (!Validator.TryValidateObject(queryConfig, new ValidationContext(queryConfig), validationResults, validateAllProperties: true)) + { + var errors = new StringBuilder($"Configuration validation failed for query {queryName}:"); + foreach (var validationResult in validationResults) + { + var members = string.Join(", ", validationResult.MemberNames); + errors.AppendLine().Append($" - {validationResult.ErrorMessage}. Members: {members}"); + } + + var errorMessage = errors.ToString(); + _errorStateHandler.Terminate(errorMessage); + throw new InvalidProgramException(errorMessage); + } + + _logger.LogDebug("Validated Query configuration for '{QueryName}'.", queryName); + } + + _logger.LogInformation("Validated query configurations."); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/Makefile b/reactions/dapr/post-output-binding/Makefile new file mode 100644 index 00000000..8b202256 --- /dev/null +++ b/reactions/dapr/post-output-binding/Makefile @@ -0,0 +1,34 @@ +.PHONY: default docker-build kind-load k3d-load test clean + +CLUSTER_NAME ?= kind +IMAGE_PREFIX ?= drasi-project +DOCKER_TAG_VERSION ?= latest +DOCKERX_OPTS ?= --load --cache-to type=inline,mode=max + +default: docker-build + +# Build the Docker image for the reaction +docker-build: + docker buildx build . --no-cache -t $(IMAGE_PREFIX)/reaction-post-dapr-output-binding:$(DOCKER_TAG_VERSION) $(DOCKERX_OPTS) -f Dockerfile + +# Load the built image into the specified Kind cluster +kind-load: + kind load docker-image $(IMAGE_PREFIX)/reaction-post-dapr-output-binding:$(DOCKER_TAG_VERSION) --name $(CLUSTER_NAME) + +# Load the built image into the specified k3d cluster +k3d-load: CLUSTER_NAME=k3s-default +k3d-load: + k3d image import $(IMAGE_PREFIX)/reaction-post-dapr-output-binding:$(DOCKER_TAG_VERSION) -c $(CLUSTER_NAME) + +# Run unit tests +test: + dotnet test post-dapr-output-binding.sln + +lint-check: + @echo "No lint checks to run yet" + +# Clean build artifacts +clean: + dotnet clean post-dapr-output-binding.sln + rm -rf */bin */obj + rm -rf Drasi.Reactions.PostDaprOutputBinding.Tests/TestResults \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/README.md b/reactions/dapr/post-output-binding/README.md new file mode 100644 index 00000000..94e848fa --- /dev/null +++ b/reactions/dapr/post-output-binding/README.md @@ -0,0 +1,80 @@ +# Post Dapr Output Binding Reaction + +This reaction forwards Drasi query results to Dapr Output Binding. It allows mapping each Drasi query to a specific Dapr Output Binding component with a specified operation. + +## Features + +- Maps Drasi queries to Dapr Output Binding +- Supports both packed and unpacked event formats (unpacked is default, using Drasi native format) +- Forwards both change events and control signals +- Configurable per query +- Automatic tracking of query failure states +- Validation of configurations at startup + +## Configuration + +The reaction is configured using JSON for each query. The configuration includes: + +| Parameter | Description | Default | Required | +|-----------|-------------|---------|----------| +| `bindingName` | Name of the Dapr Output Binding component | `drasi-binding` | Yes | +| `bindingType` | The type of the Output Binding | - | Yes | +| `bindingOperation` | The operation to run | - | Yes | +| `packed` | Whether to send events in packed format (`true`) or unpacked (`false`) | `false` | No | +| `maxFailureCount` | Max failures before query is marked as failed | `5` | No | +| `skipControlSignals` | Skip publishing control signals | `false` | No | + +### Example Configuration + +```yaml +kind: Reaction +apiVersion: v1 +name: post-dapr-output-binding +spec: + kind: PostDaprOutputBinding + properties: + # No global properties needed for this reaction + queries: + example-query: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "put" + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": false + } + another-query: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "get" + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": false + } + control-signals-skipped: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "get" + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": true + } +``` + +## Event Formats + +### Packed vs. Unpacked + +- **Packed format**: The entire ChangeEvent or ControlEvent is sent as a single message to the topic. +- **Unpacked format (Default)**: Individual messages are created for each change or control signal. + +### Drasi Native Format (for Unpacked Events) + +The native Drasi format uses these operation types: +- Insert operations (I): For new data added +- Update operations (U): For data changes +- Delete operations (D): For data removed +- Control signals (X): For system events diff --git a/reactions/dapr/post-output-binding/post-dapr-output-binding.sln b/reactions/dapr/post-output-binding/post-dapr-output-binding.sln new file mode 100644 index 00000000..26964eec --- /dev/null +++ b/reactions/dapr/post-output-binding/post-dapr-output-binding.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drasi.Reactions.PostDaprOutputBinding", "Drasi.Reactions.PostDaprOutputBinding\Drasi.Reactions.PostDaprOutputBinding.csproj", "{1F2F43DE-37F6-4701-81B6-8C7B81CB3DF7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drasi.Reactions.PostDaprOutputBinding.Tests", "Drasi.Reactions.PostDaprOutputBinding.Tests\Drasi.Reactions.PostDaprOutputBinding.Tests.csproj", "{36B32281-FF13-4DFB-892F-167DBCDD4363}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1F2F43DE-37F6-4701-81B6-8C7B81CB3DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F2F43DE-37F6-4701-81B6-8C7B81CB3DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F2F43DE-37F6-4701-81B6-8C7B81CB3DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F2F43DE-37F6-4701-81B6-8C7B81CB3DF7}.Release|Any CPU.Build.0 = Release|Any CPU + {36B32281-FF13-4DFB-892F-167DBCDD4363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36B32281-FF13-4DFB-892F-167DBCDD4363}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36B32281-FF13-4DFB-892F-167DBCDD4363}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36B32281-FF13-4DFB-892F-167DBCDD4363}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/reactions/dapr/post-output-binding/reaction-provider.yaml b/reactions/dapr/post-output-binding/reaction-provider.yaml new file mode 100644 index 00000000..cd4adf87 --- /dev/null +++ b/reactions/dapr/post-output-binding/reaction-provider.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ReactionProvider +name: PostDaprOutputBinding +spec: + services: + reaction: + image: reaction-post-dapr-output-binding + config_schema: + type: object + properties: + metadata: + type: object \ No newline at end of file diff --git a/reactions/dapr/post-output-binding/reaction.yaml b/reactions/dapr/post-output-binding/reaction.yaml new file mode 100644 index 00000000..24aefcd6 --- /dev/null +++ b/reactions/dapr/post-output-binding/reaction.yaml @@ -0,0 +1,35 @@ +kind: Reaction +apiVersion: v1 +name: post-dapr-output-binding +spec: + kind: PostDaprOutputBinding + properties: + # No global properties needed for this reaction + queries: + example-query: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "put", + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": false + } + another-query: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "get", + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": false + } + control-signals-skipped: | + { + "bindingName": "drasi-output-binding", + "bindingType": "http", + "bindingOperation": "get", + "packed": false, + "maxFailureCount": 5, + "skipControlSignals": true + }