diff --git a/.gitignore b/.gitignore index 56901ec..6a393b6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ dist .env /.envrc +/specifications.xlsx +~* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e0d1298 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,248 @@ +# AI Agent Guide for NHS Notify Supplier Config + +This document provides helpful context and commands for AI agents working on this project. + +## Project Structure + +This is a monorepo containing: + +- **packages/events** - Event schemas and domain models using Zod +- **packages/event-builder** - Tools for parsing Excel specs and generating reports +- **lambdas/** - Lambda function implementations +- **infrastructure/** - Terraform infrastructure as code +- **docs/** - Documentation site + +## Event Builder Package + +### Quick Start Commands + +```bash +# Navigate to event-builder +cd packages/event-builder + +# Run tests +npm test + +# Generate supplier reports from Excel file +npm run cli:report -- -f specifications.xlsx + +# Generate reports with custom output directory +npm run cli:report -- -f specifications.xlsx -o ./output + +# Populate DynamoDB with events +npm run cli:dynamodb -- -f specifications.xlsx + +# Generate CloudEvents +npm run cli:events -- -f specifications.xlsx +``` + +### Excel File Structure + +The `specifications.xlsx` file contains sheets: + +- **PackSpecification** - Pack specifications with postage, assembly, constraints +- **LetterVariant** - Letter variants referencing pack specs +- **VolumeGroup** - Volume groups (contracts) +- **Supplier** - Supplier definitions +- **SupplierAllocation** - Supplier allocations to volume groups +- **SupplierPack** - Supplier pack approvals and status + +See `EXCEL_HEADERS.md` for detailed field documentation. + +### Supplier Reports + +HTML reports are generated per supplier showing: + +- Assigned pack specifications +- Table of Contents with two sections: + - **Submitted & Approved Packs**: Packs with approval status SUBMITTED, PROOF_RECEIVED, APPROVED, REJECTED, or DISABLED + - **Draft Packs**: Packs with approval status DRAFT +- Both sections are sorted by pack specification ID +- Approval status and environment status +- Full pack details with tooltips from schema metadata +- Volume group allocations + +**Location**: `packages/event-builder/supplier-reports/` + +**Tooltips**: The reports use schema metadata from Zod `.meta()` calls to provide context-sensitive tooltips on hover for fields like: + +- Version number +- Delivery days +- Max weight/thickness +- Paper colour options + +### Testing + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm test -- --watch + +# Run specific test file +npm test -- parse-excel.test.ts +``` + +## Schema Metadata System + +### How Tooltips Work + +1. Domain schemas define metadata using `.meta()`: + + ```typescript + deliveryDays: z.number().optional().meta({ + title: "Delivery Days", + description: "The expected number of days for delivery under this postage option." + }) + ``` + +2. The `.meta()` method automatically registers metadata in Zod's global registry + +3. Retrieve metadata using `.meta()` without arguments: + + ```typescript + const description = $Postage.shape.deliveryDays.meta()?.description; + ``` + +4. Supplier reports use `data-tooltip` attributes to display custom CSS tooltips (not browser default `title` tooltips) + +### Key Schema Files + +- `packages/events/src/domain/pack-specification.ts` - Main pack spec schema +- `packages/events/src/domain/letter-variant.ts` - Letter variant schema +- `packages/events/src/domain/common.ts` - Common types (EnvironmentStatus, etc.) + +## Common Tasks + +### Updating Field Names + +1. Update domain schema in `packages/events/src/domain/` +2. Update Excel parsing in `packages/event-builder/src/lib/parse-excel.ts` +3. Update supplier reports in `packages/event-builder/src/lib/supplier-report.ts` +4. Update `EXCEL_HEADERS.md` documentation +5. Update test files in `__tests__/` directories +6. Run tests: `npm test` + +### Adding New Tooltips + +1. Add `.meta()` to schema field: + + ```typescript + fieldName: z.string().meta({ + title: "Display Title", + description: "Tooltip text shown on hover" + }) + ``` + +2. In supplier-report.ts, access metadata: + + ```typescript + $Schema.shape.fieldName.meta()?.description + ``` + +3. Use in HTML with `data-tooltip` attribute and `has-tooltip` class + +### Editing Markdown Files + +When editing markdown files (`.md`), always run the markdown linter to ensure formatting compliance: + +```bash +# Check a specific markdown file +./scripts/githooks/check-markdown-format.sh AGENTS.md + +# Check with all rules enabled +check=all ./scripts/githooks/check-markdown-format.sh README.md +``` + +Common markdown rules to follow: + +- Add blank lines before and after lists +- Add blank lines before and after fenced code blocks +- Use angle brackets for bare URLs: `` +- Keep consistent list markers and indentation + +### Debugging Excel Parsing + +If Excel parsing fails: + +1. Check sheet names match expected names (case-sensitive) +2. Verify required columns exist in `parse-excel.ts` interfaces +3. Check data types (dates, numbers, enums) +4. Run with debugger: `node --inspect-brk node_modules/.bin/ts-node src/cli/events.ts` + +## Workspace Commands + +```bash +# Install all dependencies +npm install + +# Run tests for specific workspace +npm test --workspace=packages/events +npm test --workspace=packages/event-builder + +# Build all packages +npm run build --workspaces + +# Lint all code +npm run lint + +# Check markdown formatting +./scripts/githooks/check-markdown-format.sh + +# Check markdown formatting for all files +check=all ./scripts/githooks/check-markdown-format.sh +``` + +## Helpful Links + +- Zod documentation: +- Zod 4 metadata: Use `.meta()` with/without arguments +- CloudEvents spec: + +## Troubleshooting + +### "Input file not found" Error + +The default Excel file path is `example_specifications.xlsx`. Use `-f` flag to specify: + +```bash +npm run cli:report -- -f specifications.xlsx +``` + +### "Sheet not found" Error + +Check that the Excel file has all required sheets: + +- PackSpecification +- LetterVariant +- VolumeGroup +- Supplier +- SupplierAllocation +- SupplierPack + +### Validation Errors + +Check the error output for specific Zod validation failures. Common issues: + +- Invalid enum values (e.g., using "PUBLISHED" instead of "PROD") +- Missing required fields +- Wrong data types (string vs number) +- Invalid date formats + +### Tooltips Not Showing + +1. Check if field has `.meta({ description: "..." })` in schema +2. Verify `data-tooltip` attribute is in HTML +3. Verify `has-tooltip` class is applied +4. Check browser console for CSS errors + +## Agent Notes + +- The project uses Zod 4.x for schema validation +- Metadata is stored in Zod's global registry via `.meta()` +- Excel parsing is forgiving - missing optional fields are handled gracefully +- Reports are regenerated from scratch each time (not incremental) +- CSS tooltips use `data-tooltip` to avoid duplicate browser tooltips +- Test files should be updated whenever domain models change +- Always run markdown linter when editing `.md` files to catch formatting issues diff --git a/README.md b/README.md index 3997b5c..7d4666d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ # NHS Notify Supplier Config -This repository contains code and schemas for NHS Notify supplier configuration management and event publishing. +This repository contains code and schemas for NHS Notify supplier configuration management. ## Purpose -- **Configuration Model:** Defines and manages supplier, quota, routing, and related configuration for NHS Notify - suppliers. -- **Event Schemas:** Publishes configuration changes as events to an event bus for consumption by other system - components. +- **Configuration Model:** Defines and manages supplier configuration for NHS Notify suppliers. +- **Event Schemas:** Domain models and event builders for publishing configuration changes as CloudEvents. ## Design @@ -21,95 +19,131 @@ A phased approach will be used to improve supplier configuration management: Configuration entities include: -- `supplier_quota`, `channel_supplier`, `queue`, `suppression_filter`, `govuknotify_account` +- Suppliers (print/letter suppliers with capacity and status) +- Volume Groups (time-based allocation periods) +- Pack Specifications (letter pack definitions with postage, assembly, and constraints) +- Letter Variants (channel and campaign-specific letter configurations) +- Supplier Allocations (percentage allocations per volume group) +- Supplier Packs (supplier approval status for pack specifications) Configuration changes are validated, auditable, and published to environments via an event bus. -### Event Publishing +## Packages -Configuration changes are published as events to a central event bus, enabling decoupled updates across bounded -contexts (core, print supplier API, template/routing UI, user management, etc.). +This branch contains the core packages: -Event publishing strategies include: +### Core Libraries -- CLI tools (tactical) -- Admin/Web UI (strategic, single source of truth) +- **`@supplier-config/event-builder`** - Builds CloudEvents from domain objects (LetterVariant, PackSpecification, Supplier, VolumeGroup, SupplierAllocation, SupplierPack) -## Event Builder CLI +### Schema Package -The Excel parsing and event publishing functionality now lives in the `event-builder` package, exposed via a single CLI with subcommands. +- **`@nhsdigital/nhs-notify-event-schemas-supplier-config`** - Domain models and event schemas (Zod) -### Commands +## Event Builder Usage -- `parse` – Parse an Excel specification file and emit JSON to stdout (packs + variants). -- `publish` – Build specialised (non-draft) LetterVariant events and publish them to an AWS EventBridge bus. +The event builder package provides functions to construct CloudEvents from domain objects. -### Quick Start +```typescript +import { + buildLetterVariantEvents, + buildPackSpecificationEvents, + buildSupplierEvents, + buildVolumeGroupEvents, + buildSupplierAllocationEvents, + buildSupplierPackEvents, +} from "@supplier-config/event-builder"; -```bash -# Parse -npm run cli:events --workspace=nhs-notify-supplier-config-event-builder -- parse -f ./specifications.xlsx +// Build events from domain objects +const letterVariantEvents = buildLetterVariantEvents(variants, startingCounter); +const packEvents = buildPackSpecificationEvents(packs, startingCounter); +const supplierEvents = buildSupplierEvents(suppliers, startingCounter); +const volumeGroupEvents = buildVolumeGroupEvents(volumeGroups, startingCounter); +const allocationEvents = buildSupplierAllocationEvents(allocations, startingCounter); +const supplierPackEvents = buildSupplierPackEvents(supplierPacks, startingCounter); +``` -# Dry-run publish (no AWS calls) -npm run cli:events --workspace=nhs-notify-supplier-config-event-builder -- publish -f ./specifications.xlsx -b my-bus --dry-run +### Configuration -# Publish (requires AWS credentials with events:PutEvents) -npm run cli:events --workspace=nhs-notify-supplier-config-event-builder -- publish -f ./specifications.xlsx -b my-bus -r eu-west-2 -``` +Event source is configured via environment variables: + +- `EVENT_ENV` - Environment identifier (default: `dev`) +- `EVENT_SERVICE` - Service identifier (default: `events`) +- `EVENT_DATASCHEMAVERSION` - Schema version (default: from schema package) -### Envelope Defaults +Source format: `/control-plane/supplier-config//` + +## Development + +### Installation + +```bash +npm install +``` -Source: `/control-plane/supplier-config//` built from `EVENT_ENV` (default `dev`) and `EVENT_SERVICE` (default `events`). +### Testing -Other generated fields: +```bash +# Run all tests +npm run test:unit -- `severitytext` INFO / `severitynumber` 2 -- `partitionkey` LetterVariant id -- `sequence` Incrementing zero-padded 20-digit counter per run -- `traceparent` Random W3C trace context value -- `dataschema` & `dataschemaversion` fixed to example `1.0.0` +# Test specific package +npm run test:unit --workspace @supplier-config/event-builder +``` -Set environment overrides: +### Linting ```bash -EVENT_ENV=staging EVENT_SERVICE=config npm run cli:events --workspace=nhs-notify-supplier-config-event-builder -- publish -f specs.xlsx -b staging-bus -r eu-west-2 +# Lint all packages +npm run lint + +# Fix linting issues +npm run lint:fix ``` -## Usage +### Type Checking -### Testing +```bash +npm run typecheck +``` -There are `make` tasks for you to configure to run your tests. Run -`make test` to see how they work. You should be able to use the same -entry points for local development as in your CI pipeline. +## Event Structure + +All events follow the CloudEvents specification with the following envelope: + +- `specversion` - CloudEvents spec version (1.0) +- `id` - Unique event ID (UUID) +- `source` - Event source path (e.g., `/control-plane/supplier-config/dev/events`) +- `subject` - Entity path (e.g., `letter-variant/`) +- `type` - Event type based on entity and status +- `time` - Event timestamp +- `datacontenttype` - application/json +- `dataschema` - Schema URL +- `dataschemaversion` - Schema version +- `data` - Event payload (the domain object) +- `traceparent` - W3C trace context +- `recordedtime` - Recording timestamp +- `severitytext` - Severity level (INFO) +- `severitynumber` - Numeric severity (2) +- `partitionkey` - Partition key for ordering +- `sequence` - Sequence number for ordering ## Contributing -Describe or link templates on how to raise an issue, feature request -or make a contribution to the codebase. Reference the other -documentation files, like +Describe or link templates on how to raise an issue, feature request or make a contribution to the codebase. Reference the other documentation files, like - Environment setup for contribution, i.e. `CONTRIBUTING.md` -- Coding standards, branching, linting, practices for development and - testing +- Coding standards, branching, linting, practices for development and testing - Release process, versioning, changelog - Backlog, board, roadmap, ways of working - High-level requirements, guiding principles, decision records, etc. ## Contacts -Provide a way to contact the owners of this project. It can be a team, -an individual or information on the means of getting in touch via -active communication channels, e.g. opening a GitHub discussion, -raising an issue, etc. +Provide a way to contact the owners of this project. It can be a team, an individual or information on the means of getting in touch via active communication channels, e.g. opening a GitHub discussion, raising an issue, etc. ## Licence -Unless stated otherwise, the codebase is released under the MIT -License. This covers both the codebase and any sample code in the -documentation. +Unless stated otherwise, the codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation. -Any HTML or Markdown documentation -is [© Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) -and available under the terms of -the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). +Any HTML or Markdown documentation is [© Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) and available under the terms of the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). diff --git a/package-lock.json b/package-lock.json index 996d51e..df68a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,726 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.980.0.tgz", + "integrity": "sha512-1rGhAx4cHZy3pMB3R3r84qMT5WEvQ6ajr2UksnD48fjQxwaUcpI6NsPvU5j/5BI5LqGiUO6ThOrMwSMm95twQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/dynamodb-codec": "^3.972.5", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz", + "integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz", + "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.2", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz", + "integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz", + "integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz", + "integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-env": "^3.972.3", + "@aws-sdk/credential-provider-http": "^3.972.5", + "@aws-sdk/credential-provider-login": "^3.972.3", + "@aws-sdk/credential-provider-process": "^3.972.3", + "@aws-sdk/credential-provider-sso": "^3.972.3", + "@aws-sdk/credential-provider-web-identity": "^3.972.3", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz", + "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz", + "integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.3", + "@aws-sdk/credential-provider-http": "^3.972.5", + "@aws-sdk/credential-provider-ini": "^3.972.3", + "@aws-sdk/credential-provider-process": "^3.972.3", + "@aws-sdk/credential-provider-sso": "^3.972.3", + "@aws-sdk/credential-provider-web-identity": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz", + "integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz", + "integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.980.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/token-providers": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz", + "integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.5.tgz", + "integrity": "sha512-gFR4w3dIkaZ82kFFjil7RFtukS2y2fXrDNDfgc94DhKjjOQMJEcHM5o1GGaQE4jd2mOQfHvbeQ0ktU8xGXhHjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@smithy/core": "^3.22.0", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "3.980.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz", + "integrity": "sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz", + "integrity": "sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.2", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz", + "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@smithy/core": "^3.22.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz", + "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz", + "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.980.0.tgz", + "integrity": "sha512-jG/yzr/JLFl7II9TTDWRKJRHThTXYNDYy694bRTj7JCXCU/Gb11ir5fJ7sV6FhlR9LrIaDb7Fft3RifvEnZcSQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "3.980.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", + "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz", + "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz", + "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.2.tgz", + "integrity": "sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "dev": true, @@ -569,6 +1289,13 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "dev": true, @@ -766,34 +1493,86 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, "engines": { - "node": ">=18.18.0" + "node": ">=12.10.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, "engines": { - "node": ">=18.18.0" + "node": ">=6" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" }, - "funding": { + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } @@ -2048,6 +2827,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@next/eslint-plugin-next": { "version": "15.4.5", "dev": true, @@ -2146,6 +2936,80 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, @@ -2177,6 +3041,600 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", + "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.11", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", + "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.22.1", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", + "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", + "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", + "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.22.1", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", + "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.32", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", + "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.11", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", + "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.9", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "3.1.0", "dev": true, @@ -2244,6 +3702,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@supplier-config/cli-dynamodb-load": { + "resolved": "packages/cli-dynamodb-load", + "link": true + }, + "node_modules/@supplier-config/event-builder": { + "resolved": "packages/event-builder", + "link": true + }, + "node_modules/@supplier-config/excel-parser": { + "resolved": "packages/excel-parser", + "link": true + }, "node_modules/@swc/core": { "version": "1.12.6", "dev": true, @@ -2393,12 +3863,35 @@ "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.47", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.47.tgz", + "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" } }, "node_modules/@types/estree": { @@ -2536,6 +4029,43 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "dev": true, @@ -2546,6 +4076,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/xlsx": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz", + "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "dev": true, @@ -2794,6 +4331,19 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.15.0", "dev": true, @@ -2833,6 +4383,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "dev": true, @@ -2875,7 +4434,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2883,7 +4441,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2907,6 +4464,44 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/arg": { "version": "4.1.3", "dev": true, @@ -3069,6 +4664,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "dev": true, @@ -3087,6 +4692,13 @@ "node": ">= 0.4" } }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "dev": true, @@ -3122,6 +4734,21 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "30.2.0", "dev": true, @@ -3216,6 +4843,192 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "dev": true, @@ -3285,11 +5098,56 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "dev": true, @@ -3301,6 +5159,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "dev": true, @@ -3388,6 +5256,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -3411,6 +5292,13 @@ "node": ">=10" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/ci-info": { "version": "4.2.0", "dev": true, @@ -3451,7 +5339,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -3471,6 +5358,15 @@ "node": ">= 0.12.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "dev": true, @@ -3478,7 +5374,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3489,7 +5384,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -3508,7 +5402,24 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 12.0.0" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" } }, "node_modules/concat-map": { @@ -3538,6 +5449,54 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/create-jest": { "version": "29.7.0", "dev": true, @@ -4262,12 +6221,6 @@ "dev": true, "license": "MIT" }, - "node_modules/csv-parse": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", - "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", - "license": "MIT" - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "dev": true, @@ -4343,7 +6296,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4453,6 +6408,114 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/docker-compose": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.1.tgz", + "integrity": "sha512-rF0wH69G3CCcmkN9J1RVMQBaKe8o77LT/3XmqcLIltWWVxcWAzp2TnO7wS3n/umZHN3/EVrlT3exSBMal+Ou1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.9.tgz", + "integrity": "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -4587,9 +6650,18 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "6.0.1", "dev": true, @@ -4811,7 +6883,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5730,6 +7801,36 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "dev": true, @@ -5914,6 +8015,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "dev": true, @@ -5950,6 +8058,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "dev": true, @@ -6094,6 +8220,22 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -6161,7 +8303,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6198,6 +8339,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "dev": true, @@ -6483,6 +8637,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "dev": true, @@ -6751,7 +8926,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10350,6 +12524,59 @@ "node": ">=0.10" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/lcov-result-merger": { "version": "5.0.1", "dev": true, @@ -10444,6 +12671,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -10454,6 +12688,13 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "dev": true, @@ -10609,11 +12850,35 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/ms": { "version": "2.1.3", "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/napi-postinstall": { "version": "0.2.4", "dev": true, @@ -10642,10 +12907,6 @@ "resolved": "lambdas/example-lambda", "link": true }, - "node_modules/nhs-notify-supplier-config-event-builder": { - "resolved": "packages/event-builder", - "link": true - }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, @@ -10787,6 +13048,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -11127,6 +13394,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "dev": true, @@ -11154,6 +13438,67 @@ "dev": true, "license": "MIT" }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", + "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/psl": { "version": "1.15.0", "dev": true, @@ -11165,6 +13510,17 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "dev": true, @@ -11217,6 +13573,46 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/refa": { "version": "0.12.1", "dev": true, @@ -11312,7 +13708,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11385,6 +13780,16 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "dev": true, @@ -11434,6 +13839,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "dev": true, @@ -11684,11 +14110,70 @@ "source-map": "^0.6.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "dev": true, @@ -11733,6 +14218,28 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "dev": true, @@ -11747,7 +14254,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11875,7 +14381,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11937,6 +14442,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -11978,6 +14495,33 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "dev": true, @@ -12030,6 +14574,40 @@ "node": "*" } }, + "node_modules/testcontainers": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz", + "integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.47", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.3.0", + "dockerode": "^4.0.9", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5", + "undici": "^7.16.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "dev": true, @@ -12069,6 +14647,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -12268,6 +14856,12 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.20.3", "dev": true, @@ -12286,6 +14880,13 @@ "fsevents": "~2.3.3" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -12439,6 +15040,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", + "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.8.0", "dev": true, @@ -12531,6 +15142,27 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, @@ -12734,6 +15366,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -12744,7 +15394,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12812,6 +15461,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "dev": true, @@ -12835,7 +15505,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -12846,9 +15515,24 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -12865,7 +15549,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -12890,6 +15573,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/zod": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", @@ -12899,38 +15597,45 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "packages/event-builder": { - "name": "nhs-notify-supplier-config-event-builder", + "packages/cli-dynamodb-load": { + "name": "@supplier-config/cli-dynamodb-load", "version": "0.0.1", "dependencies": { - "@nhsdigital/nhs-notify-event-schemas-supplier-config": "1.0.0", - "csv-parse": "^5.6.0", - "zod": "^4.1.12" + "@aws-sdk/client-dynamodb": "^3.969.0", + "@aws-sdk/util-dynamodb": "^3.969.0", + "@supplier-config/excel-parser": "*", + "yargs": "^17.7.2" + }, + "bin": { + "dynamodb-load": "dist/cli.js" }, "devDependencies": { "@swc/core": "^1.11.13", "@swc/jest": "^0.2.37", "@tsconfig/node22": "^22.0.2", "@types/jest": "^29.5.14", - "esbuild": "^0.25.9", + "@types/yargs": "^17.0.32", "jest": "^29.7.0", "jest-mock-extended": "^3.0.7", + "testcontainers": "^11.11.0", "typescript": "^5.9.3" } }, - "packages/event-builder/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "packages/event-builder": { + "name": "@supplier-config/event-builder", + "version": "0.0.1", + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-config": "*", + "zod": "^4.1.12" + }, + "devDependencies": { + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.7", + "typescript": "^5.9.3" } }, "packages/event-builder/node_modules/@jest/schemas": { @@ -12966,46 +15671,6 @@ "dev": true, "license": "MIT" }, - "packages/event-builder/node_modules/esbuild": { - "version": "0.25.9", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, "packages/event-builder/node_modules/jest": { "version": "30.2.0", "dev": true, @@ -13044,7 +15709,7 @@ "ts-jest": "^29.4.0", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "zod-mermaid": "^1.1.0" + "zod-mermaid": "^1.3.0" } }, "packages/events/node_modules/@types/node": { @@ -13072,6 +15737,28 @@ "engines": { "node": ">=18.0.0" } + }, + "packages/excel-parser": { + "name": "@supplier-config/excel-parser", + "version": "0.0.1", + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-config": "*", + "xlsx": "^0.18.5", + "yargs": "^17.7.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "@types/xlsx": "^0.0.35", + "@types/yargs": "^17.0.33", + "jest": "^29.7.0", + "jest-html-reporter": "^3.10.2", + "jest-mock-extended": "^3.0.7", + "typescript": "^5.9.3" + } } } } diff --git a/packages/cli-dynamodb-load/README.md b/packages/cli-dynamodb-load/README.md new file mode 100644 index 0000000..ef4a51f --- /dev/null +++ b/packages/cli-dynamodb-load/README.md @@ -0,0 +1,47 @@ +# DynamoDB Load CLI + +A command-line tool for populating a DynamoDB table with NHS Notify supplier configuration data from an Excel file. + +## Installation + +This package is part of the nhs-notify-supplier-config monorepo. Install dependencies from the root: + +```bash +npm install +``` + +## Usage + +```bash +# From the package directory +npm run cli -- -f specs.xlsx -t my-config-table -r eu-west-2 + +# Dry run (preview items without writing) +npm run cli -- -f specs.xlsx -t my-table --dry-run +``` + +### Options + +| Option | Alias | Description | Required | +|--------|-------|-------------|----------| +| `--file` | `-f` | Excel file path | No (default: specifications.xlsx) | +| `--table` | `-t` | DynamoDB table name | Yes | +| `--region` | `-r` | AWS region (fallback: AWS_REGION env) | No | +| `--dry-run` | | Preview items without writing | No | + +## DynamoDB Table Design + +This tool uses a single-table design with the following PK/SK patterns: + +| Entity | PK | SK | +|--------|----|----| +| VolumeGroup | `VOLUME_GROUP` | `` | +| Supplier | `SUPPLIER` | `` | +| PackSpecification | `PACK_SPECIFICATION` | `` | +| LetterVariant | `LETTER_VARIANT` | `` | +| SupplierAllocation | `SUPPLIER_ALLOCATION` | `` | +| SupplierPack | `SUPPLIER_PACK` | `` | + +## Dependencies + +- `@nhs-notify/excel-parser` - For parsing Excel files diff --git a/packages/cli-dynamodb-load/jest.config.ts b/packages/cli-dynamodb-load/jest.config.ts new file mode 100644 index 0000000..8c1d3bb --- /dev/null +++ b/packages/cli-dynamodb-load/jest.config.ts @@ -0,0 +1,28 @@ +import type { Config } from "jest"; + +const config: Config = { + moduleNameMapper: { + "^@supplier-config/cli-dynamodb-load/(.*)$": "/src/$1", + }, + preset: "ts-jest", + setupFiles: ["/src/__tests__/testcontainers-setup.ts"], + testEnvironment: "node", + testMatch: ["**/__tests__/**/*.test.ts"], + testPathIgnorePatterns: ["/node_modules/", "/dist/"], + transform: { + "^.+\\.tsx?$": [ + "@swc/jest", + { + jsc: { + parser: { + syntax: "typescript", + tsx: false, + }, + target: "es2022", + }, + }, + ], + }, +}; + +export default config; diff --git a/packages/cli-dynamodb-load/package.json b/packages/cli-dynamodb-load/package.json new file mode 100644 index 0000000..90b552d --- /dev/null +++ b/packages/cli-dynamodb-load/package.json @@ -0,0 +1,35 @@ +{ + "bin": { + "dynamodb-load": "dist/cli.js" + }, + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.969.0", + "@aws-sdk/util-dynamodb": "^3.969.0", + "@supplier-config/excel-parser": "*", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "@types/yargs": "^17.0.32", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.7", + "testcontainers": "^11.11.0", + "typescript": "^5.9.3" + }, + "name": "@supplier-config/cli-dynamodb-load", + "private": true, + "scripts": { + "build": "tsc", + "cli": "ts-node src/cli.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "jest", + "test:integration": "TESTCONTAINERS_RYUK_DISABLED=true jest --testPathPatterns=integration", + "test:unit": "jest --testPathIgnorePatterns=integration", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/packages/cli-dynamodb-load/src/__tests__/dynamodb-populate.integration.test.ts b/packages/cli-dynamodb-load/src/__tests__/dynamodb-populate.integration.test.ts new file mode 100644 index 0000000..c08e1ed --- /dev/null +++ b/packages/cli-dynamodb-load/src/__tests__/dynamodb-populate.integration.test.ts @@ -0,0 +1,423 @@ +import { + BatchWriteItemCommand, + CreateTableCommand, + DeleteTableCommand, + DynamoDBClient, + ScanCommand, +} from "@aws-sdk/client-dynamodb"; +import type { AttributeValue } from "@aws-sdk/client-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { GenericContainer, Wait } from "testcontainers"; +import { + type PopulateResult, + populateDynamoDB, +} from "../dynamodb-populate"; +import type { ParseResult } from "@supplier-config/excel-parser"; + +const TABLE_NAME = "supplier-config-test"; + +type EntityType = + | "VOLUME_GROUP" + | "SUPPLIER" + | "PACK_SPECIFICATION" + | "LETTER_VARIANT" + | "SUPPLIER_ALLOCATION" + | "SUPPLIER_PACK"; + +interface DynamoDBItem { + PK: string; + SK: string; + entityType: EntityType; + data: Record; + createdAt: string; + updatedAt: string; +} + +function chunkArray(arr: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + chunks.push(arr.slice(i, i + size)); + } + return chunks; +} + +function buildItemsFromData(data: ParseResult): DynamoDBItem[] { + const items: DynamoDBItem[] = []; + const now = new Date().toISOString(); + + for (const volumeGroup of Object.values(data.volumeGroups)) { + items.push({ + PK: "VOLUME_GROUP", + SK: volumeGroup.id, + entityType: "VOLUME_GROUP", + data: volumeGroup as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + for (const supplier of Object.values(data.suppliers)) { + items.push({ + PK: "SUPPLIER", + SK: supplier.id, + entityType: "SUPPLIER", + data: supplier as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + for (const pack of Object.values(data.packs)) { + items.push({ + PK: "PACK_SPECIFICATION", + SK: pack.id, + entityType: "PACK_SPECIFICATION", + data: pack as unknown as Record, + createdAt: pack.createdAt, + updatedAt: pack.updatedAt, + }); + } + + for (const variant of Object.values(data.variants)) { + items.push({ + PK: "LETTER_VARIANT", + SK: variant.id, + entityType: "LETTER_VARIANT", + data: variant as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + for (const allocation of Object.values(data.allocations)) { + items.push({ + PK: "SUPPLIER_ALLOCATION", + SK: allocation.id, + entityType: "SUPPLIER_ALLOCATION", + data: allocation as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + for (const supplierPack of Object.values(data.supplierPacks)) { + items.push({ + PK: "SUPPLIER_PACK", + SK: supplierPack.id, + entityType: "SUPPLIER_PACK", + data: supplierPack as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + return items; +} + +/** + * Helper function to populate DynamoDB with a custom endpoint. + * This is needed for testing with dynamodb-local. + */ +async function populateDynamoDBWithEndpoint( + data: ParseResult, + tableName: string, + endpointUrl: string, +): Promise { + const items = buildItemsFromData(data); + + const summary: Record = { + VOLUME_GROUP: 0, + SUPPLIER: 0, + PACK_SPECIFICATION: 0, + LETTER_VARIANT: 0, + SUPPLIER_ALLOCATION: 0, + SUPPLIER_PACK: 0, + }; + + for (const item of items) { + summary[item.entityType] += 1; + } + + const client = new DynamoDBClient({ + region: "us-west-2", + endpoint: endpointUrl, + credentials: { + accessKeyId: "fakeMyKeyId", + secretAccessKey: "fakeSecretAccessKey", + }, + }); + + const batches = chunkArray(items, 25); + + for (const batch of batches) { + const writeRequests = batch.map((item) => ({ + PutRequest: { + Item: marshall( + { + PK: item.PK, + SK: item.SK, + entityType: item.entityType, + ...item.data, + _createdAt: item.createdAt, + _updatedAt: item.updatedAt, + }, + { removeUndefinedValues: true }, + ), + }, + })); + + await client.send( + new BatchWriteItemCommand({ + RequestItems: { + [tableName]: writeRequests, + }, + }), + ); + } + + client.destroy(); + + return { + itemCount: items.length, + tableName, + summary, + }; +} + +function createTestData(): ParseResult { + return { + volumeGroups: { + vg1: { + id: "vg-001", + name: "Volume Group 1", + description: "Test volume group", + startDate: "2024-01-01", + status: "PUBLISHED", + }, + }, + suppliers: { + sup1: { + id: "sup-001", + name: "Supplier One", + channelType: "LETTER", + dailyCapacity: 10_000, + status: "PUBLISHED", + }, + sup2: { + id: "sup-002", + name: "Supplier Two", + channelType: "LETTER", + dailyCapacity: 5000, + status: "PUBLISHED", + }, + }, + packs: { + pack1: { + id: "pack-001", + name: "Standard Pack", + status: "PUBLISHED", + version: 1, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-02T00:00:00.000Z", + postage: { + id: "postage-001", + size: "STANDARD", + }, + }, + }, + variants: { + var1: { + id: "var-001", + name: "Letter Variant 1", + description: "Test variant", + volumeGroupId: "vg-001", + type: "TRANSACTIONAL", + status: "PUBLISHED", + packSpecificationIds: ["pack-001"], + }, + }, + allocations: { + alloc1: { + id: "alloc-001", + volumeGroup: "vg-001", + supplier: "sup-001", + allocationPercentage: 70, + status: "PUBLISHED", + }, + alloc2: { + id: "alloc-002", + volumeGroup: "vg-001", + supplier: "sup-002", + allocationPercentage: 30, + status: "PUBLISHED", + }, + }, + supplierPacks: { + sp1: { + id: "sp-001", + packSpecificationId: "pack-001", + supplierId: "sup-001", + approval: "APPROVED", + status: "PUBLISHED", + }, + }, + } as unknown as ParseResult; +} + +async function setupDynamoDBContainer() { + const container = await new GenericContainer("amazon/dynamodb-local") + .withExposedPorts(8000) + .withStartupTimeout(60_000) + .withWaitStrategy(Wait.forListeningPorts()) + .start(); + + const endpoint = `http://${container.getHost()}:${container.getMappedPort(8000)}`; + + const ddbClient = new DynamoDBClient({ + region: "us-west-2", + endpoint, + credentials: { + accessKeyId: "fakeMyKeyId", + secretAccessKey: "fakeSecretAccessKey", + }, + }); + + return { + container, + ddbClient, + endpoint, + }; +} + +type DBContext = Awaited>; + +const createTableCommand = new CreateTableCommand({ + TableName: TABLE_NAME, + BillingMode: "PAY_PER_REQUEST", + KeySchema: [ + { AttributeName: "PK", KeyType: "HASH" }, + { AttributeName: "SK", KeyType: "RANGE" }, + ], + AttributeDefinitions: [ + { AttributeName: "PK", AttributeType: "S" }, + { AttributeName: "SK", AttributeType: "S" }, + ], +}); + +async function createTable(context: DBContext) { + await context.ddbClient.send(createTableCommand); +} + +async function deleteTable(context: DBContext) { + await context.ddbClient.send( + new DeleteTableCommand({ + TableName: TABLE_NAME, + }), + ); +} + +describe("dynamodb-populate integration", () => { + let context: DBContext; + + beforeAll(async () => { + context = await setupDynamoDBContainer(); + await createTable(context); + }, 120_000); + + afterAll(async () => { + if (context) { + try { + await deleteTable(context); + } catch { + // Ignore cleanup errors + } + context.ddbClient.destroy(); + await context.container.stop(); + } + }); + + it("populates DynamoDB with all entity types", async () => { + const testData = createTestData(); + + const result = await populateDynamoDBWithEndpoint( + testData, + TABLE_NAME, + context.endpoint, + ); + + expect(result.itemCount).toBe(8); + expect(result.tableName).toBe(TABLE_NAME); + expect(result.summary).toEqual({ + VOLUME_GROUP: 1, + SUPPLIER: 2, + PACK_SPECIFICATION: 1, + LETTER_VARIANT: 1, + SUPPLIER_ALLOCATION: 2, + SUPPLIER_PACK: 1, + }); + + // Verify data was written + const scanResult = await context.ddbClient.send( + new ScanCommand({ TableName: TABLE_NAME }), + ); + + expect(scanResult.Items).toHaveLength(8); + + const items = + scanResult.Items?.map((item: Record) => + unmarshall(item), + ) ?? []; + + // Check that we have correct PK/SK combinations + const volumeGroups = items.filter( + (i: Record) => i.PK === "VOLUME_GROUP", + ); + expect(volumeGroups).toHaveLength(1); + expect(volumeGroups[0].SK).toBe("vg-001"); + expect(volumeGroups[0].name).toBe("Volume Group 1"); + + const suppliers = items.filter( + (i: Record) => i.PK === "SUPPLIER", + ); + expect(suppliers).toHaveLength(2); + + const packSpecs = items.filter( + (i: Record) => i.PK === "PACK_SPECIFICATION", + ); + expect(packSpecs).toHaveLength(1); + expect(packSpecs[0].SK).toBe("pack-001"); + + const letterVariants = items.filter( + (i: Record) => i.PK === "LETTER_VARIANT", + ); + expect(letterVariants).toHaveLength(1); + + const supplierAllocs = items.filter( + (i: Record) => i.PK === "SUPPLIER_ALLOCATION", + ); + expect(supplierAllocs).toHaveLength(2); + + const supplierPacks = items.filter( + (i: Record) => i.PK === "SUPPLIER_PACK", + ); + expect(supplierPacks).toHaveLength(1); + }, 60_000); + + it("handles dry run mode without writing", async () => { + const testData = createTestData(); + + // Clear console.log for dry run output + const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {}); + + const result = await populateDynamoDB(testData, { + tableName: "nonexistent-table", + dryRun: true, + }); + + consoleSpy.mockRestore(); + + expect(result.itemCount).toBe(8); + expect(result.summary.VOLUME_GROUP).toBe(1); + expect(result.summary.SUPPLIER).toBe(2); + }); +}); diff --git a/packages/cli-dynamodb-load/src/__tests__/testcontainers-setup.ts b/packages/cli-dynamodb-load/src/__tests__/testcontainers-setup.ts new file mode 100644 index 0000000..78b1496 --- /dev/null +++ b/packages/cli-dynamodb-load/src/__tests__/testcontainers-setup.ts @@ -0,0 +1,3 @@ +// Setup file for integration tests +// Disable Ryuk (reaper) as it has issues with some Docker socket configurations +process.env.TESTCONTAINERS_RYUK_DISABLED = "true"; diff --git a/packages/cli-dynamodb-load/src/cli.ts b/packages/cli-dynamodb-load/src/cli.ts new file mode 100644 index 0000000..a319729 --- /dev/null +++ b/packages/cli-dynamodb-load/src/cli.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env ts-node +import path from "node:path"; +import fs from "node:fs"; +import { hideBin } from "yargs/helpers"; +import yargs from "yargs"; +import { parseExcelFile } from "@supplier-config/excel-parser"; +import { populateDynamoDB } from "./dynamodb-populate"; + +interface DynamoDBArgs { + file: string; + table: string; + region?: string; + dryRun?: boolean; +} + +function ensureFile(file: string): string { + const resolved = path.isAbsolute(file) + ? file + : path.join(process.cwd(), file); + // Basic allowlist check: must end with .xlsx + if (!/\.xlsx$/i.test(resolved)) { + throw new Error(`Input file must be an .xlsx file: ${resolved}`); + } + try { + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.statSync(resolved); + } catch { + throw new Error(`Input file not found: ${resolved}`); + } + return resolved; +} + +async function handleDynamoDB(args: DynamoDBArgs): Promise { + const inputFile = ensureFile(args.file); + console.log(`Reading Excel file: ${inputFile}`); + const data = parseExcelFile(inputFile); + + console.log(`Populating DynamoDB table: ${args.table}`); + const result = await populateDynamoDB(data, { + tableName: args.table, + region: args.region, + dryRun: args.dryRun, + }); + + console.log(`\nPopulation summary:`); + console.log(` Table: ${result.tableName}`); + console.log(` Total items: ${result.itemCount}`); + console.log(` By type:`); + for (const [type, count] of Object.entries(result.summary)) { + if (count > 0) { + console.log(` - ${type}: ${count}`); + } + } +} + +async function main(): Promise { + const parser = yargs(hideBin(process.argv)) + .scriptName("dynamodb-load") + .demandCommand(0) + .strict() + .version(false) + .help() + .option("file", { + alias: "f", + describe: "Excel file path", + type: "string", + default: "specifications.xlsx", + }) + .option("table", { + alias: "t", + type: "string", + describe: "DynamoDB table name", + demandOption: true, + }) + .option("region", { + alias: "r", + type: "string", + describe: "AWS region (fallback AWS_REGION env)", + }) + .option("dry-run", { + type: "boolean", + describe: "Build items but do not write to DynamoDB", + default: false, + }) + .example( + "$0 -f specs.xlsx -t my-config-table -r eu-west-2", + "Populate DynamoDB table with config data", + ) + .example( + "$0 -f specs.xlsx -t my-table --dry-run", + "Preview items that would be written", + ); + + try { + const argv = await parser.parseAsync(); + await handleDynamoDB({ + file: argv.file, + table: argv.table, + region: argv.region, + dryRun: argv.dryRun, + }); + } catch (error) { + console.error((error as Error).message); + process.exitCode = 1; + } +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} + +export { handleDynamoDB }; diff --git a/packages/cli-dynamodb-load/src/dynamodb-populate.ts b/packages/cli-dynamodb-load/src/dynamodb-populate.ts new file mode 100644 index 0000000..8905e9a --- /dev/null +++ b/packages/cli-dynamodb-load/src/dynamodb-populate.ts @@ -0,0 +1,228 @@ +import { + BatchWriteItemCommand, + DynamoDBClient, + type WriteRequest, +} from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import type { ParseResult } from "@supplier-config/excel-parser"; + +/** + * Single-table design for supplier config. + * + * PK and SK patterns: + * - VolumeGroup: PK = "VOLUME_GROUP" SK = "" + * - Supplier: PK = "SUPPLIER" SK = "" + * - PackSpecification: PK = "PACK_SPEC" SK = "" + * - LetterVariant: PK = "LETTER_VARIANT" SK = "" + * - SupplierAllocation: PK = "SUPPLIER_ALLOC" SK = "" + * - SupplierPack: PK = "SUPPLIER_PACK" SK = "" + */ + +type EntityType = + | "VOLUME_GROUP" + | "SUPPLIER" + | "PACK_SPECIFICATION" + | "LETTER_VARIANT" + | "SUPPLIER_ALLOCATION" + | "SUPPLIER_PACK"; + +interface DynamoDBItem { + PK: string; + SK: string; + entityType: EntityType; + data: Record; + createdAt: string; + updatedAt: string; +} + +function buildItems(data: ParseResult): DynamoDBItem[] { + const items: DynamoDBItem[] = []; + const now = new Date().toISOString(); + + // Volume Groups + for (const volumeGroup of Object.values(data.volumeGroups)) { + items.push({ + PK: "VOLUME_GROUP", + SK: volumeGroup.id, + entityType: "VOLUME_GROUP", + data: volumeGroup as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + // Suppliers + for (const supplier of Object.values(data.suppliers)) { + items.push({ + PK: "SUPPLIER", + SK: supplier.id, + entityType: "SUPPLIER", + data: supplier as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + // Pack Specifications + for (const pack of Object.values(data.packs)) { + items.push({ + PK: "PACK_SPECIFICATION", + SK: pack.id, + entityType: "PACK_SPECIFICATION", + data: pack as unknown as Record, + createdAt: pack.createdAt, + updatedAt: pack.updatedAt, + }); + } + + // Letter Variants + for (const variant of Object.values(data.variants)) { + items.push({ + PK: "LETTER_VARIANT", + SK: variant.id, + entityType: "LETTER_VARIANT", + data: variant as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + // Supplier Allocations + for (const allocation of Object.values(data.allocations)) { + items.push({ + PK: "SUPPLIER_ALLOCATION", + SK: allocation.id, + entityType: "SUPPLIER_ALLOCATION", + data: allocation as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + // Supplier Packs + for (const supplierPack of Object.values(data.supplierPacks)) { + items.push({ + PK: "SUPPLIER_PACK", + SK: supplierPack.id, + entityType: "SUPPLIER_PACK", + data: supplierPack as unknown as Record, + createdAt: now, + updatedAt: now, + }); + } + + return items; +} + +function chunkArray(arr: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + chunks.push(arr.slice(i, i + size)); + } + return chunks; +} + +export interface PopulateOptions { + tableName: string; + region?: string; + dryRun?: boolean; +} + +export interface PopulateResult { + itemCount: number; + tableName: string; + summary: Record; +} + +export async function populateDynamoDB( + data: ParseResult, + options: PopulateOptions, +): Promise { + const items = buildItems(data); + + const summary: Record = { + VOLUME_GROUP: 0, + SUPPLIER: 0, + PACK_SPECIFICATION: 0, + LETTER_VARIANT: 0, + SUPPLIER_ALLOCATION: 0, + SUPPLIER_PACK: 0, + }; + + for (const item of items) { + summary[item.entityType] += 1; + } + + if (options.dryRun) { + console.log("Dry run mode - items would be written:"); + console.log(JSON.stringify(items.slice(0, 3), null, 2)); + if (items.length > 3) { + console.log(`... and ${items.length - 3} more items`); + } + return { + itemCount: items.length, + tableName: options.tableName, + summary, + }; + } + + const region = + options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; + + if (!region) { + throw new Error( + "AWS region not specified (--region flag or AWS_REGION env)", + ); + } + + const client = new DynamoDBClient({ region }); + + // DynamoDB BatchWriteItem supports max 25 items per call + const batches = chunkArray(items, 25); + + for (const batch of batches) { + const writeRequests: WriteRequest[] = batch.map((item) => ({ + PutRequest: { + Item: marshall( + { + PK: item.PK, + SK: item.SK, + entityType: item.entityType, + ...item.data, + _createdAt: item.createdAt, + _updatedAt: item.updatedAt, + }, + { removeUndefinedValues: true }, + ), + }, + })); + + const command = new BatchWriteItemCommand({ + RequestItems: { + [options.tableName]: writeRequests, + }, + }); + + try { + const response = await client.send(command); + + // Handle unprocessed items (retry logic could be added here) + const unprocessed = response.UnprocessedItems?.[options.tableName]; + if (unprocessed && unprocessed.length > 0) { + console.warn( + `Warning: ${unprocessed.length} items were not processed in this batch`, + ); + } + } catch (error) { + throw new Error( + `Failed to write batch to DynamoDB: ${(error as Error).message}`, + ); + } + } + + return { + itemCount: items.length, + tableName: options.tableName, + summary, + }; +} diff --git a/packages/cli-dynamodb-load/src/index.ts b/packages/cli-dynamodb-load/src/index.ts new file mode 100644 index 0000000..deee583 --- /dev/null +++ b/packages/cli-dynamodb-load/src/index.ts @@ -0,0 +1,5 @@ +export { populateDynamoDB } from "./dynamodb-populate"; +export type { + PopulateOptions, + PopulateResult, +} from "./dynamodb-populate"; diff --git a/packages/cli-dynamodb-load/tsconfig.json b/packages/cli-dynamodb-load/tsconfig.json new file mode 100644 index 0000000..61430a1 --- /dev/null +++ b/packages/cli-dynamodb-load/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "isolatedModules": true, + "module": "commonjs", + "outDir": "dist", + "paths": { + "@supplier-config/excel-parser": [ + "../excel-parser/src/index.ts" + ], + "@supplier-config/excel-parser/*": [ + "../excel-parser/src/*" + ] + }, + "resolveJsonModule": true, + "rootDir": "src" + }, + "exclude": [ + "node_modules", + "dist" + ], + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*" + ], + "references": [ + { + "path": "../excel-parser" + } + ] +} diff --git a/packages/event-builder/README.md b/packages/event-builder/README.md new file mode 100644 index 0000000..6a4ee11 --- /dev/null +++ b/packages/event-builder/README.md @@ -0,0 +1,115 @@ +# Event Builder + +Provides utilities for constructing CloudEvents from NHS Notify supplier configuration domain objects. + +## Installation + +This package is part of the nhs-notify-supplier-config monorepo. Install dependencies from the root: + +```bash +npm install +``` + +## Usage + +```typescript +import { + buildLetterVariantEvent, + buildLetterVariantEvents, + buildPackSpecificationEvent, + buildPackSpecificationEvents, + buildSupplierEvent, + buildSupplierEvents, + buildSupplierAllocationEvent, + buildSupplierAllocationEvents, + buildSupplierPackEvent, + buildSupplierPackEvents, + buildVolumeGroupEvent, + buildVolumeGroupEvents, +} from "@nhs-notify/event-builder"; + +// Build a single event +const event = buildLetterVariantEvent(letterVariant); + +// Build events from a record of entities +const events = buildLetterVariantEvents(variants, startingCounter); +``` + +## Event Builders + +### Letter Variant Events + +```typescript +buildLetterVariantEvent(variant: LetterVariant, options?: BuildLetterVariantEventOptions): LetterVariantSpecialisedEvent | undefined +buildLetterVariantEvents(variants: Record, startingCounter?: number): LetterVariantSpecialisedEvent[] +``` + +### Pack Specification Events + +```typescript +buildPackSpecificationEvent(pack: PackSpecification, options?: BuildPackSpecificationEventOptions): PackSpecificationSpecialisedEvent | undefined +buildPackSpecificationEvents(packs: Record, startingCounter?: number): PackSpecificationSpecialisedEvent[] +``` + +### Supplier Events + +```typescript +buildSupplierEvent(supplier: Supplier, options?: BuildSupplierEventOptions): SupplierEvent +buildSupplierEvents(suppliers: Record, startingCounter?: number): SupplierEvent[] +``` + +### Supplier Allocation Events + +```typescript +buildSupplierAllocationEvent(allocation: SupplierAllocation, options?: BuildSupplierAllocationEventOptions): SupplierAllocationSpecialisedEvent +buildSupplierAllocationEvents(allocations: Record, startingCounter?: number): SupplierAllocationSpecialisedEvent[] +``` + +### Supplier Pack Events + +```typescript +buildSupplierPackEvent(supplierPack: SupplierPack, options?: BuildSupplierPackEventOptions): SupplierPackSpecialisedEvent | undefined +buildSupplierPackEvents(supplierPacks: Record, startingCounter?: number): SupplierPackSpecialisedEvent[] +``` + +### Volume Group Events + +```typescript +buildVolumeGroupEvent(volumeGroup: VolumeGroup, options?: BuildVolumeGroupEventOptions): VolumeGroupEvent | undefined +buildVolumeGroupEvents(volumeGroups: Record, startingCounter?: number): (VolumeGroupEvent | undefined)[] +``` + +## Event Envelope + +All events follow the CloudEvents specification with the following envelope: + +| Field | Description | +|-------|-------------| +| `specversion` | CloudEvents spec version (1.0) | +| `id` | Unique event ID (UUID) | +| `source` | Event source path | +| `subject` | Entity path (e.g., `letter-variant/`) | +| `type` | Event type based on entity and status | +| `time` | Event timestamp | +| `datacontenttype` | application/json | +| `dataschema` | Schema URL | +| `dataschemaversion` | Schema version | +| `data` | Event payload (the domain object) | +| `traceparent` | W3C trace context | +| `recordedtime` | Recording timestamp | +| `severitytext` | Severity level (INFO) | +| `severitynumber` | Numeric severity (2) | +| `partitionkey` | Partition key for ordering | +| `sequence` | Sequence number for ordering | + +## Configuration + +Event source is configured via environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `EVENT_ENV` | `dev` | Environment identifier | +| `EVENT_SERVICE` | `events` | Service identifier | +| `EVENT_DATASCHEMAVERSION` | (from schema package) | Schema version | + +Source format: `/control-plane/supplier-config//` diff --git a/packages/event-builder/jest.config.ts b/packages/event-builder/jest.config.ts index 0bcda43..31376e1 100644 --- a/packages/event-builder/jest.config.ts +++ b/packages/event-builder/jest.config.ts @@ -2,13 +2,50 @@ import type { Config } from "jest"; const config: Config = { preset: "ts-jest", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "./.reports/unit/coverage", + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "babel", + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ["/__tests__/"], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], + + moduleNameMapper: { + "^@supplier-config/event-builder/(.*)$": "/src/$1", + }, + testEnvironment: "node", testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], moduleFileExtensions: ["ts", "js", "json", "node"], - moduleNameMapper: { - "@nhsdigital/nhs-notify-supplier-config-schemas$": - "/../../packages/schemas/src", - }, }; export default config; diff --git a/packages/event-builder/package.json b/packages/event-builder/package.json index 3e454e7..1619f57 100644 --- a/packages/event-builder/package.json +++ b/packages/event-builder/package.json @@ -1,20 +1,18 @@ { "dependencies": { - "@nhsdigital/nhs-notify-supplier-config-schemas": "1.0.0", - "csv-parse": "^5.6.0", - "zod": "^4.0.17" + "@nhsdigital/nhs-notify-event-schemas-supplier-config": "*", + "zod": "^4.1.12" }, "devDependencies": { "@swc/core": "^1.11.13", "@swc/jest": "^0.2.37", "@tsconfig/node22": "^22.0.2", "@types/jest": "^29.5.14", - "esbuild": "^0.25.9", "jest": "^29.7.0", "jest-mock-extended": "^3.0.7", - "typescript": "^5.8.2" + "typescript": "^5.9.3" }, - "name": "nhs-notify-supplier-config-event-builder", + "name": "@supplier-config/event-builder", "private": true, "scripts": { "build": "tsc", diff --git a/packages/event-builder/src/__tests__/base-event-envelope.test.ts b/packages/event-builder/src/__tests__/base-event-envelope.test.ts new file mode 100644 index 0000000..c26c24d --- /dev/null +++ b/packages/event-builder/src/__tests__/base-event-envelope.test.ts @@ -0,0 +1,109 @@ +import { buildBaseEventEnvelope } from "../lib/base-event-envelope"; +import { configFromEnv } from "../config"; + +describe("buildBaseEventEnvelope", () => { + it("applies default severity INFO when none provided", () => { + const cfg = configFromEnv(); + const evt = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + { foo: "bar" }, + "https://example.test/schema.json", + cfg, + {}, + ); + expect(evt.severitytext).toBe("INFO"); + expect(evt.severitynumber).toBe(2); + }); + + it("applies explicit severity DEBUG", () => { + const cfg = configFromEnv(); + const evt = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + { foo: "bar" }, + "https://example.test/schema.json", + cfg, + { severity: "DEBUG" }, + ); + expect(evt.severitytext).toBe("DEBUG"); + expect(evt.severitynumber).toBe(1); + }); + + it("uses generator for sequence values", () => { + const cfg = configFromEnv(); + // eslint-disable-next-line unicorn/consistent-function-scoping + function* g(): Generator { + for (;;) { + yield "g-1"; + yield "g-2"; + } + } + const gen = g(); + const first = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + {}, + "https://example.test/schema.json", + cfg, + { sequence: gen }, + ); + const second = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + {}, + "https://example.test/schema.json", + cfg, + { sequence: gen }, + ); + expect(first.sequence).toBe("g-1"); + expect(second.sequence).toBe("g-2"); + }); + + it("uses provided literal sequence string", () => { + const cfg = configFromEnv(); + const evt = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + {}, + "https://example.test/schema.json", + cfg, + { sequence: "literal-seq" }, + ); + expect(evt.sequence).toBe("literal-seq"); + }); + + it("handles undefined sequence (no property mutation)", () => { + const cfg = configFromEnv(); + const evt = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + {}, + "https://example.test/schema.json", + cfg, + {}, + ); + expect(evt.sequence).toBeUndefined(); + }); + + it("accepts an undefined options object", () => { + const cfg = configFromEnv(); + const evt = buildBaseEventEnvelope( + "test.type", + "test/subject", + "pk-1", + {}, + "https://example.test/schema.json", + cfg, + ); + expect(evt.severitytext).toBe("INFO"); + expect(evt.severitynumber).toBe(2); + expect(evt.sequence).toBeUndefined(); + }); +}); diff --git a/packages/event-builder/src/__tests__/config.test.ts b/packages/event-builder/src/__tests__/config.test.ts new file mode 100644 index 0000000..4cc19e4 --- /dev/null +++ b/packages/event-builder/src/__tests__/config.test.ts @@ -0,0 +1,45 @@ +import packageJson from "@nhsdigital/nhs-notify-event-schemas-supplier-config/package.json"; +import { Config, buildEventSource, configFromEnv } from "../config"; + +describe("config.ts", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; // shallow copy + delete process.env.EVENT_ENV; + delete process.env.EVENT_SERVICE; + delete process.env.EVENT_DATASCHEMAVERSION; + }); + + afterAll(() => { + process.env = originalEnv; // restore + }); + + it("returns defaults when env vars are not set", () => { + const cfg = configFromEnv(); + expect(cfg.EVENT_ENV).toBe("dev"); + expect(cfg.EVENT_SERVICE).toBe("events"); + expect(cfg.EVENT_DATASCHEMAVERSION).toBe(packageJson.version); + }); + + it("applies overrides from environment variables", () => { + process.env.EVENT_ENV = "prod"; + process.env.EVENT_SERVICE = "supplier"; + process.env.EVENT_DATASCHEMAVERSION = "9.9.9"; + const cfg = configFromEnv(); + expect(cfg.EVENT_ENV).toBe("prod"); + expect(cfg.EVENT_SERVICE).toBe("supplier"); + expect(cfg.EVENT_DATASCHEMAVERSION).toBe("9.9.9"); + }); + + it("buildEventSource constructs expected path", () => { + const cfg: Config = { + EVENT_ENV: "int", + EVENT_SERVICE: "allocations", + EVENT_DATASCHEMAVERSION: packageJson.version, + }; + const src = buildEventSource(cfg); + expect(src).toBe("/control-plane/supplier-config/int/allocations"); + }); +}); diff --git a/packages/event-builder/src/__tests__/letter-variant-event-builder.test.ts b/packages/event-builder/src/__tests__/letter-variant-event-builder.test.ts new file mode 100644 index 0000000..fbf5d51 --- /dev/null +++ b/packages/event-builder/src/__tests__/letter-variant-event-builder.test.ts @@ -0,0 +1,87 @@ +import { LetterVariant } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src"; +import { + buildLetterVariantEvent, + buildLetterVariantEvents, +} from "../letter-variant-event-builder"; +import { configFromEnv } from "../config"; + +describe("letter-variant-event-builder", () => { + const base: Partial = { + name: "Test Variant", + description: "Test", + volumeGroupId: "volume-group-123" as any, + type: "STANDARD", + packSpecificationIds: ["00000000-0000-0000-0000-000000000001" as any], + clientId: "client-1", + }; + + it("skips draft", () => { + const ev = buildLetterVariantEvent({ + ...base, + id: "11111111-1111-1111-1111-111111111111" as any, + status: "DRAFT", + } as LetterVariant); + expect(ev).toBeUndefined(); + }); + + it("throws on unknown status", () => { + const variant = { + ...base, + id: "11111111-1111-1111-1111-111111111111" as any, + status: "UNKNOWN" as any, + } as unknown as LetterVariant; + expect(() => buildLetterVariantEvent(variant)).toThrow( + /No specialised event schema found for status UNKNOWN/, + ); + }); + + it("builds published", () => { + const ev = buildLetterVariantEvent({ + ...base, + id: "22222222-2222-2222-2222-222222222222" as any, + status: "PROD", + } as LetterVariant); + expect(ev).toBeDefined(); + expect(ev!.type).toMatch(/letter-variant.prod/); + expect(ev!.subject).toBe( + "letter-variant/22222222-2222-2222-2222-222222222222", + ); + expect(ev!.partitionkey).toBe("22222222-2222-2222-2222-222222222222"); + expect(ev!.severitytext).toBe("INFO"); + }); + + it("builds multiple with sequence ordering", () => { + const events = buildLetterVariantEvents({ + a: { + ...base, + id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" as any, + status: "PROD", + } as LetterVariant, + b: { + ...base, + id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" as any, + status: "INT", + } as LetterVariant, + }); + expect(events).toHaveLength(2); + expect(events[0].sequence).toBe("00000000000000000001"); + expect(events[1].sequence).toBe("00000000000000000002"); + }); + + it("applies configured dataschema version", () => { + const config = { ...configFromEnv(), EVENT_DATASCHEMAVERSION: "1.999.0" }; + const ev = buildLetterVariantEvent( + { + ...base, + id: "22222222-2222-2222-2222-222222222222" as any, + status: "PROD", + } as LetterVariant, + {}, + config, + ); + expect(ev).toBeDefined(); + expect(ev?.dataschema).toMatch( + /letter-variant.prod\.1\.999\.0\.schema\.json$/, + ); + }); +}); diff --git a/packages/event-builder/src/__tests__/pack-specification-event-builder.test.ts b/packages/event-builder/src/__tests__/pack-specification-event-builder.test.ts new file mode 100644 index 0000000..3434442 --- /dev/null +++ b/packages/event-builder/src/__tests__/pack-specification-event-builder.test.ts @@ -0,0 +1,69 @@ +import { PackSpecification } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { + buildPackSpecificationEvent, + buildPackSpecificationEvents, +} from "../pack-specification-event-builder"; + +describe("pack-specification-event-builder", () => { + const base = { + name: "Test Pack", + status: "PROD", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + version: 1, + postage: { id: "postage-test" as any, size: "STANDARD" }, + } satisfies Omit; + + it("skips draft", () => { + const ev = buildPackSpecificationEvent({ + ...base, + id: "11111111-1111-1111-1111-111111111111" as any, + status: "DRAFT", + } as PackSpecification); + expect(ev).toBeUndefined(); + }); + + it("throws on unknown status", () => { + const pack = { + ...base, + id: "11111111-1111-1111-1111-111111111111" as any, + status: "UNKNOWN" as any, + } as unknown as PackSpecification; + expect(() => buildPackSpecificationEvent(pack)).toThrow( + /No specialised event schema found for status UNKNOWN/, + ); + }); + + it("builds published", () => { + const event = buildPackSpecificationEvent({ + ...base, + id: "22222222-2222-2222-2222-222222222222" as any, + status: "PROD", + } satisfies PackSpecification); + expect(event).toBeDefined(); + expect(event!.type).toMatch(/pack-specification.prod/); + expect(event!.subject).toBe( + "pack-specification/22222222-2222-2222-2222-222222222222", + ); + expect(event!.partitionkey).toBe("22222222-2222-2222-2222-222222222222"); + expect(event!.severitytext).toBe("INFO"); + }); + + it("builds multiple with sequence ordering", () => { + const events = buildPackSpecificationEvents({ + a: { + ...base, + id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" as any, + status: "PROD", + } as PackSpecification, + b: { + ...base, + id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" as any, + status: "INT", + } as PackSpecification, + }); + expect(events).toHaveLength(2); + expect(events[0].sequence).toBe("00000000000000000001"); + expect(events[1].sequence).toBe("00000000000000000002"); + }); +}); diff --git a/packages/event-builder/src/__tests__/volume-group-event-builder.test.ts b/packages/event-builder/src/__tests__/volume-group-event-builder.test.ts new file mode 100644 index 0000000..b515011 --- /dev/null +++ b/packages/event-builder/src/__tests__/volume-group-event-builder.test.ts @@ -0,0 +1,175 @@ +import { VolumeGroup } from "@nhsdigital/nhs-notify-event-schemas-supplier-config"; +import { + buildVolumeGroupEvent, + buildVolumeGroupEvents, +} from "../volume-group-event-builder"; +import { configFromEnv } from "../config"; + +describe("volume-group-event-builder", () => { + const baseVolumeGroup: VolumeGroup = { + id: "volume-group-1", + name: "Volume Group 1", + startDate: "2025-01-01", // date only per schema + status: "PROD", + }; + const secondVolumeGroup: VolumeGroup = { + id: "volume-group-2", + name: "Volume Group 2", + startDate: "2025-01-02", // date only + status: "PROD", + }; + const draftVolumeGroup: VolumeGroup = { + id: "volume-group-draft", + name: "Volume Group Draft", + startDate: "2025-01-03", // date only + status: "DRAFT", + }; + const disabledVolumeGroup: VolumeGroup = { + id: "volume-group-disabled", + name: "Volume Group Disabled", + startDate: "2025-02-01", + status: "INT", + }; + + it("builds event with explicit sequence string and severity ERROR", () => { + const cfg = configFromEnv(); + const event = buildVolumeGroupEvent( + baseVolumeGroup, + { + severity: "ERROR", + sequence: "00000000000000000042", + }, + cfg, + ); + expect(event).toBeDefined(); + expect(event!.sequence).toBe("00000000000000000042"); + expect(event!.severitytext).toBe("ERROR"); + expect(event!.severitynumber).toBe(4); + expect(event!.subject).toBe("volume-group/volume-group-1"); + expect(event!.type).toBe( + "uk.nhs.notify.supplier-config.volume-group.prod.v1", + ); + }); + + it("builds events using generator sequence path (object branch)", () => { + const events = buildVolumeGroupEvents( + { vg1: baseVolumeGroup, vg2: secondVolumeGroup }, + 10, + ); + expect(events).toHaveLength(2); + expect(events[0]!.sequence).toBe("00000000000000000010"); + expect(events[1]!.sequence).toBe("00000000000000000011"); + // default severity INFO + expect(events[0]!.severitytext).toBe("INFO"); + expect(events[0]!.severitynumber).toBe(2); + }); + + it("builds events using generator sequence and default startingCounter", () => { + const events = buildVolumeGroupEvents({ + vg1: baseVolumeGroup, + vg2: secondVolumeGroup, + }); + expect(events).toHaveLength(2); + expect(events[0]!.sequence).toBe("00000000000000000001"); + expect(events[1]!.sequence).toBe("00000000000000000002"); + // default severity INFO + expect(events[0]!.severitytext).toBe("INFO"); + expect(events[0]!.severitynumber).toBe(2); + }); + + it("builds event without sequence (undefined branch) and severity WARN", () => { + const event = buildVolumeGroupEvent(baseVolumeGroup, { severity: "WARN" }); + expect(event).toBeDefined(); + expect(event!.sequence).toBeUndefined(); + expect(event!.severitytext).toBe("WARN"); + expect(event!.severitynumber).toBe(3); + }); + + it("applies severity FATAL mapping", () => { + const event = buildVolumeGroupEvent(baseVolumeGroup, { severity: "FATAL" }); + expect(event).toBeDefined(); + expect(event!.severitytext).toBe("FATAL"); + expect(event!.severitynumber).toBe(5); + }); + + it("returns undefined for DRAFT volume group", () => { + const event = buildVolumeGroupEvent(draftVolumeGroup); + expect(event).toBeUndefined(); + }); + + it("buildVolumeGroupEvents includes undefined for DRAFT volume group", () => { + const events = buildVolumeGroupEvents({ + published: baseVolumeGroup, + draft: draftVolumeGroup, + }); + expect(events).toHaveLength(2); + const publishedEvent = events.find( + (e) => e && e.subject === "volume-group/volume-group-1", + ); + expect(publishedEvent).toBeDefined(); + const draftEvent = events.find( + (e) => e?.subject === "volume-group/volume-group-draft", + ); + expect(draftEvent).toBeUndefined(); + expect(events.filter((e) => e === undefined)).toHaveLength(1); + }); + + it("builds event for INT status", () => { + const event = buildVolumeGroupEvent(disabledVolumeGroup); + expect(event).toBeDefined(); + expect(event!.type).toBe( + "uk.nhs.notify.supplier-config.volume-group.int.v1", + ); + expect(event!.subject).toBe("volume-group/volume-group-disabled"); + expect(event!.partitionkey).toBe(disabledVolumeGroup.id); + }); + + it("builds event with TRACE severity", () => { + const event = buildVolumeGroupEvent(baseVolumeGroup, { severity: "TRACE" }); + expect(event).toBeDefined(); + expect(event!.severitytext).toBe("TRACE"); + expect(event!.severitynumber).toBe(0); + }); + + it("builds event with DEBUG severity", () => { + const event = buildVolumeGroupEvent(baseVolumeGroup, { severity: "DEBUG" }); + expect(event).toBeDefined(); + expect(event!.severitytext).toBe("DEBUG"); + expect(event!.severitynumber).toBe(1); + }); + + it("includes partitionkey and valid traceparent format", () => { + const event = buildVolumeGroupEvent(baseVolumeGroup); + expect(event).toBeDefined(); + expect(event!.partitionkey).toBe(baseVolumeGroup.id); + expect(event!.traceparent).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/); + }); + + it("uses custom config env overrides for source and dataschema version", () => { + const originalEnv = { ...process.env }; + try { + process.env.EVENT_ENV = "staging"; + process.env.EVENT_SERVICE = "custom-service"; + process.env.EVENT_DATASCHEMAVERSION = "1.9.9"; // must start with major '1.' per schema regex + const cfg = configFromEnv(); + const event = buildVolumeGroupEvent(baseVolumeGroup, {}, cfg); + expect(event).toBeDefined(); + expect(event!.source).toBe( + "/control-plane/supplier-config/staging/custom-service", + ); + expect(event!.dataschema).toMatch( + /volume-group.prod\.1\.9\.9\.schema\.json$/, + ); + } finally { + process.env = originalEnv; // restore + } + }); + + it("throws error when specialised schema missing (unknown status)", () => { + // Force an invalid status not in volumeGroupEvents map + const bogus = { ...baseVolumeGroup, status: "ARCHIVED" as any }; + expect(() => buildVolumeGroupEvent(bogus as VolumeGroup)).toThrow( + /No specialised event schema found for status ARCHIVED/, + ); + }); +}); diff --git a/packages/event-builder/src/cli/events.ts b/packages/event-builder/src/cli/events.ts new file mode 100644 index 0000000..577b99f --- /dev/null +++ b/packages/event-builder/src/cli/events.ts @@ -0,0 +1,391 @@ +#!/usr/bin/env ts-node +import path from "node:path"; +import fs from "node:fs"; +import { hideBin } from "yargs/helpers"; +import yargs from "yargs"; +import { + EventBridgeClient, + PutEventsCommand, +} from "@aws-sdk/client-eventbridge"; +import { parseExcelFile } from "event-builder/src/lib/parse-excel"; +import { buildLetterVariantEvents } from "event-builder/src/letter-variant-event-builder"; +import { buildPackSpecificationEvents } from "event-builder/src/pack-specification-event-builder"; +import { buildVolumeGroupEvents } from "event-builder/src/volume-group-event-builder"; +import { buildSupplierEvents } from "event-builder/src/supplier-event-builder"; +import { buildSupplierAllocationEvents } from "event-builder/src/supplier-allocation-event-builder"; +import { buildSupplierPackEvents } from "event-builder/src/supplier-pack-event-builder"; +import { nextSequence } from "event-builder/src/lib/envelope-helpers"; +import generateTemplateExcel from "../lib/template"; +import { generateSupplierReports } from "../lib/supplier-report"; +import { populateDynamoDB } from "../lib/dynamodb-populate"; + +interface CommonArgs { + file: string; +} +interface PublishArgs extends CommonArgs { + bus: string; + region?: string; + dryRun?: boolean; +} +interface TemplateArgs { + out: string; + force?: boolean; +} +interface ReportArgs extends CommonArgs { + out: string; + excludeDrafts?: boolean; +} +interface DynamoDBArgs extends CommonArgs { + table: string; + region?: string; + dryRun?: boolean; +} + +function ensureFile(file: string): string { + const resolved = path.isAbsolute(file) + ? file + : path.join(process.cwd(), file); + // Basic allowlist check: must end with .xlsx + if (!/\.xlsx$/i.test(resolved)) { + throw new Error(`Input file must be an .xlsx file: ${resolved}`); + } + try { + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.statSync(resolved); + } catch { + throw new Error(`Input file not found: ${resolved}`); + } + return resolved; +} + +async function handleParse(args: CommonArgs): Promise { + const inputFile = ensureFile(args.file); + console.log(`Parsing Excel file: ${inputFile}`); + const result = parseExcelFile(inputFile); + console.log(JSON.stringify(result, null, 2)); + console.log(`Parsed ${Object.keys(result.packs).length} pack specifications`); + console.log(`Parsed ${Object.keys(result.variants).length} letter variants`); + console.log( + `Parsed ${Object.keys(result.volumeGroups).length} volume groups`, + ); + console.log(`Parsed ${Object.keys(result.suppliers).length} suppliers`); + console.log( + `Parsed ${Object.keys(result.allocations).length} supplier allocations`, + ); + console.log( + `Parsed ${Object.keys(result.supplierPacks).length} supplier packs`, + ); +} + +function chunk(arr: T[], size: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); + return out; +} + +async function handlePublish(args: PublishArgs): Promise { + const inputFile = ensureFile(args.file); + const { + allocations, + packs, + supplierPacks, + suppliers, + variants, + volumeGroups, + } = parseExcelFile(inputFile); + console.log(`Reading all entities from: ${inputFile}`); + + // Build events in sequence: volume groups, suppliers, packs, supplier-packs, variants, allocations + let counter = 1; + + const volumeGroupEventsRaw = buildVolumeGroupEvents(volumeGroups, counter); + const volumeGroupEvents = volumeGroupEventsRaw.filter( + (e): e is NonNullable => e !== undefined, + ); + counter += volumeGroupEventsRaw.length; // maintain sequence spacing including skipped drafts + + const supplierEvents = buildSupplierEvents(suppliers, counter); + counter += supplierEvents.length; + + const packEvents = buildPackSpecificationEvents(packs, counter); + counter += packEvents.length; + + const supplierPackEvents = buildSupplierPackEvents(supplierPacks, counter); + counter += supplierPackEvents.length; + + const variantEvents = buildLetterVariantEvents(variants).map((ev, idx) => { + return { ...ev, sequence: nextSequence(counter + idx) }; + }); + counter += variantEvents.length; + + const allocationEvents = buildSupplierAllocationEvents(allocations).map( + (ev, idx) => { + return { ...ev, sequence: nextSequence(counter + idx) }; + }, + ); + + const events = [ + ...volumeGroupEvents, + ...supplierEvents, + ...packEvents, + ...supplierPackEvents, + ...variantEvents, + ...allocationEvents, + ]; + + console.log( + `Built ${volumeGroupEvents.length} VolumeGroup events, ${supplierEvents.length} Supplier events, ${packEvents.length} PackSpecification events, ${supplierPackEvents.length} SupplierPack events, ${variantEvents.length} LetterVariant events, and ${allocationEvents.length} SupplierAllocation events`, + ); + + if (args.dryRun) { + console.log( + "--dry-run specified; events will NOT be sent. Showing first event:", + ); + if (events[0]) console.log(JSON.stringify(events[0], null, 2)); + return; + } + + const region = + args.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; + if (!region) + throw new Error("AWS region not specified (flag or AWS_REGION env)"); + + const client = new EventBridgeClient({ region }); + for (const batch of chunk(events, 10)) { + const Entries = batch.map((e) => ({ + DetailType: e.type, + Source: e.source, + EventBusName: args.bus, + Time: new Date(e.time), + Detail: JSON.stringify(e.data), + Resources: [e.subject], + })); + try { + const resp = await client.send(new PutEventsCommand({ Entries })); + if (resp.FailedEntryCount && resp.FailedEntryCount > 0) { + console.error(`PutEvents had ${resp.FailedEntryCount} failed entries`); + console.error(JSON.stringify(resp, null, 2)); + process.exitCode = 1; + return; + } + } catch (error) { + console.error("Error sending events batch", error); + process.exitCode = 1; + return; + } + } + console.log( + `Successfully published ${events.length} events to bus ${args.bus}`, + ); +} + +async function handleTemplate(args: TemplateArgs): Promise { + const output = generateTemplateExcel(args.out, args.force); + console.log(`Template Excel written: ${output}`); +} + +async function handleReport(args: ReportArgs): Promise { + const inputFile = ensureFile(args.file); + console.log(`Reading Excel file: ${inputFile}`); + const data = parseExcelFile(inputFile); + + const result = generateSupplierReports(data, args.out, { + excludeDrafts: args.excludeDrafts, + }); + + console.log( + `\nGenerated ${result.reports.length} supplier reports in: ${result.outputDir}\n`, + ); + for (const report of result.reports) { + console.log( + ` - ${report.supplierName}: ${report.packCount} pack(s) -> ${report.filePath}`, + ); + } +} + +async function handleDynamoDB(args: DynamoDBArgs): Promise { + const inputFile = ensureFile(args.file); + console.log(`Reading Excel file: ${inputFile}`); + const data = parseExcelFile(inputFile); + + console.log(`Populating DynamoDB table: ${args.table}`); + const result = await populateDynamoDB(data, { + tableName: args.table, + region: args.region, + dryRun: args.dryRun, + }); + + console.log(`\nPopulation summary:`); + console.log(` Table: ${result.tableName}`); + console.log(` Total items: ${result.itemCount}`); + console.log(` By type:`); + for (const [type, count] of Object.entries(result.summary)) { + if (count > 0) { + console.log(` - ${type}: ${count}`); + } + } +} + +async function main(): Promise { + const parser = yargs(hideBin(process.argv)) + .scriptName("events") + .demandCommand(1, "Specify a command") + .strict() + .recommendCommands() + .version(false) + .help() + .command( + "parse", + "Parse excel and output JSON to stdout", + (cmd) => + cmd.option("file", { + alias: "f", + describe: "Excel file path", + type: "string", + default: "example_specifications.xlsx", + }), + async (argv) => { + await handleParse({ file: argv.file }); + }, + ) + .command( + "publish", + "Publish all supplier config events (Contract, Supplier, PackSpecification, LetterVariant, SupplierAllocation) to EventBridge", + (cmd) => + cmd + .option("file", { + alias: "f", + describe: "Excel file path", + type: "string", + default: "example_specifications.xlsx", + }) + .option("bus", { + alias: "b", + type: "string", + describe: "EventBridge event bus name", + demandOption: true, + }) + .option("region", { + alias: "r", + type: "string", + describe: "AWS region (fallback AWS_REGION env)", + }) + .option("dry-run", { + type: "boolean", + describe: "Build events but do not send", + default: false, + }), + async (argv) => { + await handlePublish(argv); + }, + ) + .command( + "template", + "Generate a blank Excel template with all required sheets and columns", + (cmd) => + cmd + .option("out", { + alias: "o", + type: "string", + describe: "Output .xlsx file path", + default: "specifications.template.xlsx", + }) + .option("force", { + alias: "F", + type: "boolean", + describe: "Overwrite existing file if present", + default: false, + }), + async (argv) => { + await handleTemplate(argv); + }, + ) + .command( + "report", + "Generate HTML reports per supplier showing assigned pack specifications", + (cmd) => + cmd + .option("file", { + alias: "f", + describe: "Excel file path", + type: "string", + default: "example_specifications.xlsx", + }) + .option("out", { + alias: "o", + type: "string", + describe: "Output directory for HTML reports", + default: "./supplier-reports", + }) + .option("exclude-drafts", { + type: "boolean", + describe: "Exclude supplier packs with DRAFT approval status from the reports", + default: false, + }), + async (argv) => { + await handleReport(argv); + }, + ) + .command( + "dynamodb", + "Populate a DynamoDB table with config data from the spreadsheet", + (cmd) => + cmd + .option("file", { + alias: "f", + describe: "Excel file path", + type: "string", + default: "example_specifications.xlsx", + }) + .option("table", { + alias: "t", + type: "string", + describe: "DynamoDB table name", + demandOption: true, + }) + .option("region", { + alias: "r", + type: "string", + describe: "AWS region (fallback AWS_REGION env)", + }) + .option("dry-run", { + type: "boolean", + describe: "Build items but do not write to DynamoDB", + default: false, + }), + async (argv) => { + await handleDynamoDB(argv); + }, + ) + .example("$0 parse -f specs.xlsx", "Parse a spreadsheet and print JSON") + .example( + "$0 publish -f specs.xlsx -b my-bus -r eu-west-2", + "Publish events to EventBridge", + ) + .example( + "$0 template -o specs.xlsx", + "Generate template workbook (fails if specs.xlsx exists unless --force)", + ) + .example( + "$0 report -f specs.xlsx -o ./reports", + "Generate HTML supplier reports", + ) + .example( + "$0 dynamodb -f specs.xlsx -t my-config-table -r eu-west-2", + "Populate DynamoDB table with config data", + ); + + try { + await parser.parseAsync(); + } catch (error) { + console.error((error as Error).message); + process.exitCode = 1; + } +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/packages/event-builder/src/config.ts b/packages/event-builder/src/config.ts index c5e6b2c..8d452ca 100644 --- a/packages/event-builder/src/config.ts +++ b/packages/event-builder/src/config.ts @@ -1,12 +1,20 @@ import { z } from "zod"; +import packageJson from "@nhsdigital/nhs-notify-event-schemas-supplier-config/package.json"; + +const dataschemaversion = packageJson.version; const $Config = z.object({ - EVENT_SOURCE: z.string(), + EVENT_ENV: z.string().default("dev"), + EVENT_SERVICE: z.string().default("events"), + EVENT_DATASCHEMAVERSION: z.string().default(dataschemaversion), }); +export type Config = z.infer; -export const loadConfig = () => { +export const configFromEnv = () => { return $Config.parse(process.env); }; -export const eventSource = - "//notify.nhs.uk/app/nhs-notify-supplier-config-dev/main"; +export const buildEventSource = (config: Config) => { + const { EVENT_ENV, EVENT_SERVICE } = config; + return `/control-plane/supplier-config/${EVENT_ENV}/${EVENT_SERVICE}`; +}; diff --git a/packages/event-builder/src/index.ts b/packages/event-builder/src/index.ts new file mode 100644 index 0000000..14510c4 --- /dev/null +++ b/packages/event-builder/src/index.ts @@ -0,0 +1,68 @@ +// Config +export { configFromEnv, buildEventSource } from "./config"; +export type { Config } from "./config"; + +// Envelope helpers +export { + severityNumber, + generateTraceParent, + nextSequence, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +export type { SeverityText } from "./lib/envelope-helpers"; + +// Base event envelope +export { buildBaseEventEnvelope } from "./lib/base-event-envelope"; +export type { BaseEnvelopeOptions } from "./lib/base-event-envelope"; + +// Event builders +export { + buildLetterVariantEvent, + buildLetterVariantEvents, +} from "./letter-variant-event-builder"; +export type { + BuildLetterVariantEventOptions, + LetterVariantSpecialisedEvent, +} from "./letter-variant-event-builder"; + +export { + buildPackSpecificationEvent, + buildPackSpecificationEvents, +} from "./pack-specification-event-builder"; +export type { + BuildPackSpecificationEventOptions, + PackSpecificationSpecialisedEvent, +} from "./pack-specification-event-builder"; + +export { + buildSupplierEvent, + buildSupplierEvents, +} from "./supplier-event-builder"; +export type { + BuildSupplierEventOptions, + SupplierEvent, +} from "./supplier-event-builder"; + +export { + buildSupplierAllocationEvent, + buildSupplierAllocationEvents, +} from "./supplier-allocation-event-builder"; +export type { + BuildSupplierAllocationEventOptions, + SupplierAllocationSpecialisedEvent, +} from "./supplier-allocation-event-builder"; + +export { + buildSupplierPackEvent, + buildSupplierPackEvents, +} from "./supplier-pack-event-builder"; +export type { + BuildSupplierPackEventOptions, + SupplierPackSpecialisedEvent, +} from "./supplier-pack-event-builder"; + +export { + buildVolumeGroupEvent, + buildVolumeGroupEvents, +} from "./volume-group-event-builder"; +export type { BuildVolumeGroupEventOptions } from "./volume-group-event-builder"; diff --git a/packages/event-builder/src/letter-variant-event-builder.ts b/packages/event-builder/src/letter-variant-event-builder.ts new file mode 100644 index 0000000..f578273 --- /dev/null +++ b/packages/event-builder/src/letter-variant-event-builder.ts @@ -0,0 +1,66 @@ +import { LetterVariant } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/letter-variant"; +import { letterVariantEvents } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/letter-variant-events"; +import { z } from "zod"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; + +export interface BuildLetterVariantEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +export type LetterVariantSpecialisedEvent = z.infer< + (typeof letterVariantEvents)[keyof typeof letterVariantEvents] +>; + +export const buildLetterVariantEvent = ( + variant: LetterVariant, + opts: BuildLetterVariantEventOptions = {}, + config = configFromEnv(), +): LetterVariantSpecialisedEvent | undefined => { + if (variant.status === "DRAFT") return undefined; // skip drafts + + const lcStatus = variant.status.toLowerCase(); + const schemaKey = + `letter-variant.${lcStatus}` as keyof typeof letterVariantEvents; + // Access using controlled key constructed from validated status + // eslint-disable-next-line security/detect-object-injection + const specialised = letterVariantEvents[schemaKey]; + if (!specialised) { + throw new Error( + `No specialised event schema found for status ${variant.status}`, + ); + } + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.${lcStatus}.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + return specialised.parse( + buildBaseEventEnvelope( + specialised.shape.type.options[0], + `letter-variant/${variant.id}`, + variant.id, + { ...variant, status: variant.status }, + dataschema, + config, + { severity, sequence: opts.sequence }, + ), + ); +}; + +export const buildLetterVariantEvents = ( + variants: Record, + startingCounter = 1, +): LetterVariantSpecialisedEvent[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return ( + Object.values(variants) + .map((v) => buildLetterVariantEvent(v, { sequence: sequenceGenerator })) + // key fields are UUIDs already validated so dynamic filtering is safe + .filter((e): e is LetterVariantSpecialisedEvent => e !== undefined) + ); +}; diff --git a/packages/event-builder/src/lib/base-event-envelope.ts b/packages/event-builder/src/lib/base-event-envelope.ts new file mode 100644 index 0000000..cac3454 --- /dev/null +++ b/packages/event-builder/src/lib/base-event-envelope.ts @@ -0,0 +1,49 @@ +import { randomUUID } from "node:crypto"; +import { Config, buildEventSource } from "../config"; +import { + SeverityText, + generateTraceParent, + severityNumber, +} from "./envelope-helpers"; + +export interface BaseEnvelopeOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +// Common fields builder for CloudEvents envelope metadata +export function buildBaseEventEnvelope( + type: string, + subject: string, + partitionKey: string, + data: TData, + dataschema: string, + config: Config, + opts: BaseEnvelopeOptions = {}, +) { + const now = new Date().toISOString(); + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const severity = opts.severity ?? "INFO"; + return { + specversion: "1.0", + id: randomUUID(), + source: buildEventSource(config), + subject, + type, + plane: "control" as const, + time: now, + datacontenttype: "application/json", + dataschema, + dataschemaversion, + data, + traceparent: generateTraceParent(), + recordedtime: now, + severitytext: severity, + severitynumber: severityNumber(severity), + partitionkey: partitionKey, + sequence: + typeof opts.sequence === "object" + ? opts.sequence.next().value + : opts.sequence, + }; +} diff --git a/packages/event-builder/src/lib/envelope-helpers.ts b/packages/event-builder/src/lib/envelope-helpers.ts new file mode 100644 index 0000000..ae6a43c --- /dev/null +++ b/packages/event-builder/src/lib/envelope-helpers.ts @@ -0,0 +1,42 @@ +import { randomUUID } from "node:crypto"; + +export type SeverityText = + | "TRACE" + | "DEBUG" + | "INFO" + | "WARN" + | "ERROR" + | "FATAL"; + +const SEVERITY_MAP: Record = { + TRACE: 0, + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + FATAL: 5, +}; + +export const severityNumber = (severity: SeverityText): number => { + // eslint-disable-next-line security/detect-object-injection + return SEVERITY_MAP[severity]; +}; + +export const generateTraceParent = (): string => { + const traceId = randomUUID().replaceAll("-", ""); // 32 hex + const spanId = randomUUID().replaceAll("-", "").slice(0, 16); // 16 hex + return `00-${traceId}-${spanId}-01`; +}; + +export const nextSequence = (counter: number): string => + counter.toString().padStart(20, "0"); + +export function* newSequenceGenerator( + startingCounter: number, +): Generator { + let counter = startingCounter; + while (true) { + yield nextSequence(counter); + counter += 1; + } +} diff --git a/packages/event-builder/src/lib/supplier-report.ts b/packages/event-builder/src/lib/supplier-report.ts new file mode 100644 index 0000000..a7cf3d2 --- /dev/null +++ b/packages/event-builder/src/lib/supplier-report.ts @@ -0,0 +1,1032 @@ +import * as fs from "node:fs"; +import path from "node:path"; +import { + $PackSpecification, + $Paper, + $Postage, + PackSpecification, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { Supplier } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier"; +import { SupplierAllocation } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-allocation"; +import { SupplierPack } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-pack"; +import { VolumeGroup } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/volume-group"; +import { ParseResult } from "./parse-excel"; + +interface SupplierPackWithSpec { + packSpecification: PackSpecification; + supplierPack: SupplierPack; +} + +interface AllocationWithVolumeGroup { + allocation: SupplierAllocation; + volumeGroup: VolumeGroup; +} + +interface SupplierReport { + allocations: AllocationWithVolumeGroup[]; + packs: SupplierPackWithSpec[]; + supplier: Supplier; +} + +function escapeHtml(text: string | number | boolean | undefined): string { + if (text === undefined) return ""; + const str = String(text); + return str + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function formatValue(value: unknown): string { + if (value === undefined || value === null) return "Not specified"; + if (typeof value === "boolean") return value ? "Yes" : "No"; + if (Array.isArray(value)) return value.join(", "); + if (typeof value === "object") return JSON.stringify(value, null, 2); + return escapeHtml(String(value)); +} + +function sanitizeAnchorId(text: string): string { + return text + .toLowerCase() + .replaceAll(/[^a-z0-9]+/g, "-") + .replaceAll(/(^-)|(-$)/g, ""); +} + +function getApprovalStatusTooltip(approval: string): string { + const tooltips: Record = { + DRAFT: "Not yet submitted to the supplier for review", + SUBMITTED: + "Pack specification has been submitted to the supplier for review", + PROOF_RECEIVED: + "Supplier has returned a proof for approval based on the specification details", + APPROVED: "Pack specification has been approved and is ready for use", + REJECTED: "Pack specification has been rejected and requires revision", + DISABLED: + "No longer active and won't be available for allocating new letters to the supplier", + }; + return tooltips[approval] || ""; +} + +function getEnvironmentStatusTooltip(status: string): string { + const tooltips: Record = { + DRAFT: "In draft state, not deployed to any environment", + INT: "Deployed to integration/test environment", + PROD: "Deployed to production environment", + }; + return tooltips[status] || ""; +} + +function getPackSpecificationStatusTooltip(status: string): string { + const tooltips: Record = { + DRAFT: "Pack specification is in draft and not yet published", + INT: "Pack specification is published to integration/test environment", + PROD: "Pack specification is published to production environment", + }; + return tooltips[status] || ""; +} + +function renderOptionalRow( + label: string, + value: unknown, + formatter: (v: unknown) => string = (v) => escapeHtml(String(v)), + tooltip?: string, +): string { + if (value === undefined || value === null) return ""; + const tooltipAttr = tooltip + ? ` data-tooltip="${escapeHtml(tooltip)}" class="has-tooltip"` + : ""; + return `${label}${formatter(value)}`; +} + +function renderConstraintsSection( + constraints: PackSpecification["constraints"], +): string { + // Get the Constraints schema from PackSpecification + const constraintsSchema = $PackSpecification.shape.constraints; + const unwrapped = constraintsSchema.unwrap().shape; + + const maxSheetsTooltip = unwrapped.maxSheets.meta()?.description; + const deliveryDaysTooltip = unwrapped.deliveryDays.meta()?.description; + const blackCoverageTooltip = unwrapped.blackCoveragePercentage.meta()?.description; + const colourCoverageTooltip = unwrapped.colourCoveragePercentage.meta()?.description; + + const maxSheetsHeader = maxSheetsTooltip ? ` data-tooltip="${escapeHtml(maxSheetsTooltip)}" class="has-tooltip"` : ""; + const deliveryDaysHeader = deliveryDaysTooltip ? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"` : ""; + const blackCoverageHeader = blackCoverageTooltip ? ` data-tooltip="${escapeHtml(blackCoverageTooltip)}" class="has-tooltip"` : ""; + const colourCoverageHeader = colourCoverageTooltip ? ` data-tooltip="${escapeHtml(colourCoverageTooltip)}" class="has-tooltip"` : ""; + + return ` +

Constraints

+ + Max Sheets + Delivery Days + Black Coverage (%) + Colour Coverage (%) +
${constraints?.maxSheets !== undefined ? escapeHtml(constraints.maxSheets) : "Not specified"}
${constraints?.deliveryDays !== undefined ? escapeHtml(constraints.deliveryDays) : "Not specified"}
${constraints?.blackCoveragePercentage !== undefined ? escapeHtml(constraints.blackCoveragePercentage) : "Not specified"}
${constraints?.colourCoveragePercentage !== undefined ? escapeHtml(constraints.colourCoveragePercentage) : "Not specified"}
+ `; +} + +function renderPaperSection( + paper: NonNullable["paper"], +): string { + const colourTooltip = $Paper.shape.colour.meta()?.description; + const colourHeader = colourTooltip + ? ` data-tooltip="${escapeHtml(colourTooltip)}" class="has-tooltip"` + : ""; + + if (!paper) { + return ` +
Paper Details
+ + + + + + Colour + + +
Paper IDNot specified
Paper NameNot specified
Weight (GSM)Not specified
SizeNot specified
Not specified
FinishNot specified
RecycledNot specified
+ `; + } + + return ` +
Paper Details
+ + + + + + Colour + + +
Paper ID${escapeHtml(paper.id)}
Paper Name${escapeHtml(paper.name)}
Weight (GSM)${escapeHtml(paper.weightGSM)}
Size${escapeHtml(paper.size)}
${escapeHtml(paper.colour)}
Finish${paper.finish ? escapeHtml(paper.finish) : "Not specified"}
Recycled${paper.recycled ? "Yes" : "No"}
+ `; +} + +function renderAssemblySection( + assembly: PackSpecification["assembly"], +): string { + const envelopeId = assembly?.envelopeId ? escapeHtml(assembly.envelopeId) : "Not specified"; + const printColour = assembly?.printColour ? escapeHtml(assembly.printColour) : "Not specified"; + const duplex = assembly?.duplex !== undefined ? (assembly.duplex ? "Yes" : "No") : "Not specified"; + const features = assembly?.features && assembly.features.length > 0 ? formatValue(assembly.features) : "Not specified"; + const insertIds = assembly?.insertIds && assembly.insertIds.length > 0 ? formatValue(assembly.insertIds) : "Not specified"; + const additional = assembly?.additional ? `
${escapeHtml(JSON.stringify(assembly.additional, null, 2))}
` : "Not specified"; + + return ` +

Assembly

+ + + + + + + +
Envelope ID${envelopeId}
Print Colour${printColour}
Duplex${duplex}
Features${features}
Insert IDs${insertIds}
Additional${additional}
+ ${renderPaperSection(assembly?.paper)} + `; +} + +function generatePackDetailsHtml(pack: PackSpecification): string { + const deliveryDaysTooltip = $Postage.shape.deliveryDays.meta()?.description; + const maxWeightTooltip = $Postage.shape.maxWeightGrams.meta()?.description; + const maxThicknessTooltip = $Postage.shape.maxThicknessMm.meta()?.description; + + const deliveryDaysHeader = deliveryDaysTooltip ? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"` : ""; + const maxWeightHeader = maxWeightTooltip ? ` data-tooltip="${escapeHtml(maxWeightTooltip)}" class="has-tooltip"` : ""; + const maxThicknessHeader = maxThicknessTooltip ? ` data-tooltip="${escapeHtml(maxThicknessTooltip)}" class="has-tooltip"` : ""; + + const versionTooltip = $PackSpecification.shape.version.meta()?.description; + const versionHeader = versionTooltip + ? ` data-tooltip="${escapeHtml(versionTooltip)}" class="has-tooltip"` + : ""; + + const packStatusTooltip = getPackSpecificationStatusTooltip(pack.status); + + return ` +
+

Basic Information

+ + + + + + Version + + +
ID${escapeHtml(pack.id)}
Name${escapeHtml(pack.name)}
Description${pack.description ? escapeHtml(pack.description) : "Not specified"}
Pack Specification Status${escapeHtml(pack.status)}
${escapeHtml(pack.version)}
Created At${escapeHtml(pack.createdAt)}
Updated At${escapeHtml(pack.updatedAt)}
+ +

Postage

+ + + + Delivery Days + Max Weight (grams) + Max Thickness (mm) +
Postage ID${escapeHtml(pack.postage.id)}
Size${escapeHtml(pack.postage.size)}
${pack.postage.deliveryDays !== undefined ? escapeHtml(pack.postage.deliveryDays) : "Not specified"}
${pack.postage.maxWeightGrams !== undefined ? escapeHtml(pack.postage.maxWeightGrams) : "Not specified"}
${pack.postage.maxThicknessMm !== undefined ? escapeHtml(pack.postage.maxThicknessMm) : "Not specified"}
+ + ${renderConstraintsSection(pack.constraints)} + ${renderAssemblySection(pack.assembly)} +
+ `; +} + +function generateSupplierHtml(report: SupplierReport): string { + const { allocations, packs, supplier } = report; + const generatedAt = new Date().toISOString(); + + // Separate packs into submitted/approved vs draft + const submittedPacks = packs.filter( + (p) => p.supplierPack.approval !== "DRAFT", + ); + const draftPacks = packs.filter((p) => p.supplierPack.approval === "DRAFT"); + + // Sort both groups by pack specification ID + const sortedSubmittedPacks = [...submittedPacks].sort((a, b) => + a.packSpecification.id.localeCompare(b.packSpecification.id), + ); + const sortedDraftPacks = [...draftPacks].sort((a, b) => + a.packSpecification.id.localeCompare(b.packSpecification.id), + ); + + // Combine in order: submitted first, then drafts + const sortedPacks = [...sortedSubmittedPacks, ...sortedDraftPacks]; + + const packSections = sortedPacks + .map(({ packSpecification, supplierPack }) => { + const anchorId = `pack-${sanitizeAnchorId(packSpecification.id)}`; + const approvalTooltip = getApprovalStatusTooltip(supplierPack.approval); + const envTooltip = getEnvironmentStatusTooltip(supplierPack.status); + return ` +
+

${escapeHtml(packSpecification.name)}

+

Supplier pack approval

+
+ Approval: + ${escapeHtml(supplierPack.approval)} + Environment: + ${escapeHtml(supplierPack.status)} +
+
+

Pack Specification ID: ${escapeHtml(supplierPack.packSpecificationId)}

+
+ ${generatePackDetailsHtml(packSpecification)} + ↑ Back to top +
+ `; + }) + .join("\n"); + + // Generate Table of Contents with two sections + const generateTocItems = (packList: SupplierPackWithSpec[]) => + packList + .map(({ packSpecification, supplierPack }) => { + const anchorId = `pack-${sanitizeAnchorId(packSpecification.id)}`; + const approvalTooltip = getApprovalStatusTooltip(supplierPack.approval); + const envTooltip = getEnvironmentStatusTooltip(supplierPack.status); + return ` +
  • + ${escapeHtml(packSpecification.id)} – ${escapeHtml(packSpecification.name)} + + ${escapeHtml(supplierPack.approval)} + ${escapeHtml(supplierPack.status)} + +
  • `; + }) + .join("\n"); + + const submittedTocItems = generateTocItems(sortedSubmittedPacks); + const draftTocItems = generateTocItems(sortedDraftPacks); + + const tocSection = + packs.length > 0 + ? ` + + ` + : ""; + + const approvedCount = packs.filter( + (p) => p.supplierPack.approval === "APPROVED", + ).length; + const submittedCount = packs.filter( + (p) => p.supplierPack.approval === "SUBMITTED", + ).length; + const draftCount = packs.filter( + (p) => p.supplierPack.approval === "DRAFT", + ).length; + const prodCount = packs.filter( + (p) => p.supplierPack.status === "PROD", + ).length; + + return ` + + + + + Supplier Config Report - ${escapeHtml(supplier.name)} + + + +
    +
    +

    Supplier Config Report

    +

    Generated: ${generatedAt}

    +
    +
    + +
    +
    +

    ${escapeHtml(supplier.name)}

    + + + + + +
    Supplier ID${escapeHtml(supplier.id)}
    Channel Type${escapeHtml(supplier.channelType)}
    Daily Capacity${escapeHtml(supplier.dailyCapacity.toLocaleString())}
    Status${escapeHtml(supplier.status)}
    +
    + + ${ + allocations.length > 0 + ? ` +
    +

    Volume Group Allocations

    + + + + + + + + + + + ${allocations + .toSorted((a, b) => + a.volumeGroup.name.localeCompare(b.volumeGroup.name), + ) + .map( + ({ allocation, volumeGroup }) => ` + + + + + + + `, + ) + .join("\n")} + +
    Volume GroupPeriodAllocationStatus
    + ${escapeHtml(volumeGroup.name)} + ${volumeGroup.description ? `
    ${escapeHtml(volumeGroup.description)}` : ""} +
    ${escapeHtml(volumeGroup.startDate)}${volumeGroup.endDate ? ` to ${escapeHtml(volumeGroup.endDate)}` : " (ongoing)"}${escapeHtml(allocation.allocationPercentage)}%${escapeHtml(allocation.status)}
    +
    + ` + : "" + } + +
    +
    +
    ${packs.length}
    +
    Pack Specifications
    +
    +
    +
    ${draftCount}
    +
    Draft
    +
    +
    +
    ${submittedCount}
    +
    Submitted
    +
    +
    +
    ${approvedCount}
    +
    Approved
    +
    +
    +
    ${prodCount}
    +
    Production
    +
    +
    + + ${tocSection} + +

    Pack Specifications (${packs.length})

    + ${packSections.length > 0 ? packSections : "

    No pack specifications assigned to this supplier.

    "} +
    + +
    +

    NHS Notify Supplier Configuration Report

    +
    + +`; +} + +function buildSupplierReports(data: ParseResult, options: GenerateReportsOptions = {}): Map { + const reports = new Map(); + + // Initialize reports for all suppliers + for (const supplier of Object.values(data.suppliers)) { + reports.set(supplier.id, { + allocations: [], + packs: [], + supplier, + }); + } + + // Associate supplier allocations with their volume groups + for (const allocation of Object.values(data.allocations)) { + const report = reports.get(allocation.supplier); + if (report) { + const volumeGroup = Object.values(data.volumeGroups).find( + (vg) => vg.id === allocation.volumeGroup, + ); + if (volumeGroup) { + report.allocations.push({ + allocation, + volumeGroup, + }); + } + } + } + + // Associate supplier packs with their pack specifications + for (const supplierPack of Object.values(data.supplierPacks)) { + // Skip draft packs if excludeDrafts option is enabled + if (options.excludeDrafts && supplierPack.approval === "DRAFT") { + continue; + } + + const report = reports.get(supplierPack.supplierId); + if (report) { + const packSpecification = Object.values(data.packs).find( + (p) => p.id === supplierPack.packSpecificationId, + ); + if (packSpecification) { + report.packs.push({ + packSpecification, + supplierPack, + }); + } else { + throw new Error( + `Supplier pack ${supplierPack.id} references unknown pack specification ${supplierPack.packSpecificationId}`, + ); + } + } else { + throw new Error( + `Supplier pack ${supplierPack.id} references unknown supplier ${supplierPack.supplierId}`, + ); + } + } + + return reports; +} + +function sanitizeFilename(name: string): string { + const sanitized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-"); + // Remove leading and trailing dashes + const chars = [...sanitized]; + const firstNonDash = chars.findIndex((c) => c !== "-"); + const lastNonDash = chars.findLastIndex((c) => c !== "-"); + if (firstNonDash === -1) return ""; + return sanitized.slice(firstNonDash, lastNonDash + 1); +} + +export interface GenerateReportsOptions { + excludeDrafts?: boolean; +} + +export interface GenerateReportsResult { + outputDir: string; + reports: { + filePath: string; + packCount: number; + supplierId: string; + supplierName: string; + }[]; +} + +export function generateSupplierReports( + data: ParseResult, + outputDir: string, + options: GenerateReportsOptions = {}, +): GenerateReportsResult { + const reports = buildSupplierReports(data, options); + const result: GenerateReportsResult = { + outputDir, + reports: [], + }; + + // Ensure output directory exists + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (!fs.existsSync(outputDir)) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.mkdirSync(outputDir, { recursive: true }); + } + + for (const [supplierId, report] of reports) { + const supplierNameSanitized = sanitizeFilename(report.supplier.name); + const filename = `supplier-report-${supplierNameSanitized}.html`; + const filePath = path.join(outputDir, filename); + const html = generateSupplierHtml(report); + + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(filePath, html, "utf8"); + + result.reports.push({ + filePath, + packCount: report.packs.length, + supplierId, + supplierName: report.supplier.name, + }); + } + + return result; +} diff --git a/packages/event-builder/src/pack-specification-event-builder.ts b/packages/event-builder/src/pack-specification-event-builder.ts new file mode 100644 index 0000000..de21e1f --- /dev/null +++ b/packages/event-builder/src/pack-specification-event-builder.ts @@ -0,0 +1,63 @@ +import { PackSpecification } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { packSpecificationEvents } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/pack-specification-events"; +import { z } from "zod"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; + +export interface BuildPackSpecificationEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +export type PackSpecificationSpecialisedEvent = z.infer< + (typeof packSpecificationEvents)[keyof typeof packSpecificationEvents] +>; + +export const buildPackSpecificationEvent = ( + pack: PackSpecification, + opts: BuildPackSpecificationEventOptions = {}, + config = configFromEnv(), +): PackSpecificationSpecialisedEvent | undefined => { + if (pack.status === "DRAFT") return undefined; // skip drafts + const lcStatus = pack.status.toLowerCase(); + const schemaKey = + `pack-specification.${lcStatus}` as keyof typeof packSpecificationEvents; + // Access using controlled key constructed from validated status + // eslint-disable-next-line security/detect-object-injection + const specialised = packSpecificationEvents[schemaKey]; + if (!specialised) { + throw new Error( + `No specialised event schema found for status ${pack.status}`, + ); + } + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/pack-specification.${lcStatus}.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + const baseEvent = buildBaseEventEnvelope( + specialised.shape.type.options[0], + `pack-specification/${pack.id}`, + pack.id, + { ...pack, status: pack.status }, + dataschema, + config, + { severity, sequence: opts.sequence }, + ); + return specialised.parse(baseEvent); +}; + +export const buildPackSpecificationEvents = ( + packs: Record, + startingCounter = 1, +): PackSpecificationSpecialisedEvent[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return Object.values(packs) + .map((p) => { + return buildPackSpecificationEvent(p, { sequence: sequenceGenerator }); + }) + .filter((e): e is PackSpecificationSpecialisedEvent => e !== undefined); +}; diff --git a/packages/event-builder/src/supplier-allocation-event-builder.ts b/packages/event-builder/src/supplier-allocation-event-builder.ts new file mode 100644 index 0000000..b754cbc --- /dev/null +++ b/packages/event-builder/src/supplier-allocation-event-builder.ts @@ -0,0 +1,60 @@ +import { SupplierAllocation } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-allocation"; +import { supplierAllocationEvents } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/supplier-allocation-events"; +import { z } from "zod"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; + +export interface BuildSupplierAllocationEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +export type SupplierAllocationSpecialisedEvent = z.infer< + (typeof supplierAllocationEvents)[keyof typeof supplierAllocationEvents] +>; + +export const buildSupplierAllocationEvent = ( + allocation: SupplierAllocation, + opts: BuildSupplierAllocationEventOptions = {}, + config = configFromEnv(), +): SupplierAllocationSpecialisedEvent => { + const lcStatus = allocation.status.toLowerCase(); + const schemaKey = + `supplier-allocation.${lcStatus}` as keyof typeof supplierAllocationEvents; + // Access using controlled key constructed from validated status + // eslint-disable-next-line security/detect-object-injection + const specialised = supplierAllocationEvents[schemaKey]; + if (!specialised) { + throw new Error( + `No specialised event schema found for status ${allocation.status}`, + ); + } + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier-allocation.${lcStatus}.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + const baseEvent = buildBaseEventEnvelope( + specialised.shape.type.options[0], + `supplier-allocation/${allocation.id}`, + allocation.id, + { ...allocation, status: allocation.status }, + dataschema, + config, + { severity, sequence: opts.sequence }, + ); + return specialised.parse(baseEvent); +}; + +export const buildSupplierAllocationEvents = ( + allocations: Record, + startingCounter = 1, +): SupplierAllocationSpecialisedEvent[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return Object.values(allocations).map((a) => + buildSupplierAllocationEvent(a, { sequence: sequenceGenerator }), + ); +}; diff --git a/packages/event-builder/src/supplier-event-builder.ts b/packages/event-builder/src/supplier-event-builder.ts new file mode 100644 index 0000000..dfea58f --- /dev/null +++ b/packages/event-builder/src/supplier-event-builder.ts @@ -0,0 +1,63 @@ +import { Supplier } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; + +export interface BuildSupplierEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +// Suppliers don't have status-based events, they're just configuration +export interface SupplierEvent { + specversion: string; + id: string; + source: string; + subject: string; + type: string; + time: string; + datacontenttype: string; + dataschema: string; + dataschemaversion: string; + data: Supplier; + traceparent: string; + recordedtime: string; + severitytext: string; + severitynumber: number; + partitionkey: string; + sequence?: string; +} + +export const buildSupplierEvent = ( + supplier: Supplier, + opts: BuildSupplierEventOptions = {}, + config = configFromEnv(), +): SupplierEvent => { + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + + return buildBaseEventEnvelope( + "uk.nhs.notify.supplier-config.supplier", + `supplier/${supplier.id}`, + supplier.id, + supplier, + dataschema, + config, + { severity, sequence: opts.sequence }, + ); +}; + +export const buildSupplierEvents = ( + suppliers: Record, + startingCounter = 1, +): SupplierEvent[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return Object.values(suppliers).map((s) => + buildSupplierEvent(s, { sequence: sequenceGenerator }), + ); +}; diff --git a/packages/event-builder/src/supplier-pack-event-builder.ts b/packages/event-builder/src/supplier-pack-event-builder.ts new file mode 100644 index 0000000..1130e24 --- /dev/null +++ b/packages/event-builder/src/supplier-pack-event-builder.ts @@ -0,0 +1,68 @@ +import { SupplierPack } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-pack"; +import { supplierPackEvents } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/supplier-pack-events"; +import { z } from "zod"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; + +export interface BuildSupplierPackEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +export type SupplierPackSpecialisedEvent = z.infer< + (typeof supplierPackEvents)[keyof typeof supplierPackEvents] +>; + +export const buildSupplierPackEvent = ( + supplierPack: SupplierPack, + opts: BuildSupplierPackEventOptions = {}, + config = configFromEnv(), +): SupplierPackSpecialisedEvent | undefined => { + // Only publish APPROVED and DISABLED status events (not SUBMITTED or REJECTED) + if ( + supplierPack.approval !== "APPROVED" && + supplierPack.approval !== "DISABLED" + ) { + return undefined; + } + + const lcStatus = supplierPack.status.toLowerCase(); + const schemaKey = + `supplier-pack.${lcStatus}` as keyof typeof supplierPackEvents; + // Access using controlled key constructed from validated status + // eslint-disable-next-line security/detect-object-injection + const specialised = supplierPackEvents[schemaKey]; + if (!specialised) { + throw new Error( + `No specialised event schema found for status ${supplierPack.status}`, + ); + } + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier-pack.${lcStatus}.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + const baseEvent = buildBaseEventEnvelope( + specialised.shape.type.options[0], + `supplier-pack/${supplierPack.id}`, + supplierPack.id, + { ...supplierPack, status: supplierPack.status }, + dataschema, + config, + { severity, sequence: opts.sequence }, + ); + return specialised.parse(baseEvent); +}; + +export const buildSupplierPackEvents = ( + supplierPacks: Record, + startingCounter = 1, +): SupplierPackSpecialisedEvent[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return Object.values(supplierPacks) + .map((sp) => buildSupplierPackEvent(sp, { sequence: sequenceGenerator })) + .filter((e): e is SupplierPackSpecialisedEvent => e !== undefined); +}; diff --git a/packages/event-builder/src/volume-group-event-builder.ts b/packages/event-builder/src/volume-group-event-builder.ts new file mode 100644 index 0000000..42d3212 --- /dev/null +++ b/packages/event-builder/src/volume-group-event-builder.ts @@ -0,0 +1,64 @@ +import { VolumeGroup } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/volume-group"; +import { configFromEnv } from "./config"; +import { + SeverityText, + newSequenceGenerator, +} from "./lib/envelope-helpers"; +import { buildBaseEventEnvelope } from "./lib/base-event-envelope"; +import { z } from "zod"; +import { + $VolumeGroupEvent, + volumeGroupEvents, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config"; + +export interface BuildVolumeGroupEventOptions { + severity?: SeverityText; + sequence?: string | Generator; +} + +type VolumeGroupEvent = z.infer; + +export const buildVolumeGroupEvent = ( + volumeGroup: VolumeGroup, + opts: BuildVolumeGroupEventOptions = {}, + config = configFromEnv(), +): VolumeGroupEvent | undefined => { + if (volumeGroup.status === "DRAFT") return undefined; // skip drafts + + const lcStatus = volumeGroup.status.toLowerCase(); + const schemaKey = + `volume-group.${lcStatus}` as keyof typeof volumeGroupEvents; + // Access using controlled key constructed from validated status + // eslint-disable-next-line security/detect-object-injection + const specialised = volumeGroupEvents[schemaKey]; + if (!specialised) { + throw new Error( + `No specialised event schema found for status ${volumeGroup.status}`, + ); + } + const dataschemaversion = config.EVENT_DATASCHEMAVERSION; + const dataschema = `https://notify.nhs.uk/cloudevents/schemas/supplier-config/volume-group.${lcStatus}.${dataschemaversion}.schema.json`; + const severity = opts.severity ?? "INFO"; + return specialised.parse( + buildBaseEventEnvelope( + specialised.shape.type.options[0], + `volume-group/${volumeGroup.id}`, + volumeGroup.id, + { ...volumeGroup, status: volumeGroup.status }, + dataschema, + config, + { severity, sequence: opts.sequence }, + ), + ); +}; + +export const buildVolumeGroupEvents = ( + volumeGroups: Record, + startingCounter = 1, +): (VolumeGroupEvent | undefined)[] => { + const sequenceGenerator = newSequenceGenerator(startingCounter); + + return Object.values(volumeGroups).map((vg) => + buildVolumeGroupEvent(vg, { sequence: sequenceGenerator }), + ); +}; diff --git a/packages/event-builder/tsconfig.json b/packages/event-builder/tsconfig.json index a61a816..295f7c2 100644 --- a/packages/event-builder/tsconfig.json +++ b/packages/event-builder/tsconfig.json @@ -1,13 +1,18 @@ { "compilerOptions": { - "baseUrl": "./src/" + "declaration": true, + "isolatedModules": true, + "module": "commonjs", + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src" }, "exclude": [ - "node_modules" + "node_modules", + "dist" ], - "extends": "@tsconfig/node22/tsconfig.json", + "extends": "../../tsconfig.base.json", "include": [ - "src/**/*", - "./jest.config.ts" + "src/**/*" ] } diff --git a/packages/events/.gitignore b/packages/events/.gitignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/packages/events/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/packages/events/.spectral.yaml b/packages/events/.spectral.yaml new file mode 100644 index 0000000..63790cd --- /dev/null +++ b/packages/events/.spectral.yaml @@ -0,0 +1 @@ +extends: ["spectral:oas", "spectral:asyncapi", "spectral:arazzo"] diff --git a/packages/events/jest.config.js b/packages/events/jest.config.js new file mode 100644 index 0000000..0ef77ea --- /dev/null +++ b/packages/events/jest.config.js @@ -0,0 +1,43 @@ +module.exports = { + preset: 'ts-jest', + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: './.reports/unit/coverage', + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'babel', + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ['/__tests__/'], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + 'default', + [ + 'jest-html-reporter', + { + pageTitle: 'Test Report', + outputPath: './.reports/unit/test-report.html', + includeFailureMsg: true, + }, + ], + ], + + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*test.ts'], + moduleFileExtensions: ['ts', 'js', 'json', 'node'], +}; diff --git a/packages/schemas/package.json b/packages/events/package.json similarity index 50% rename from packages/schemas/package.json rename to packages/events/package.json index a5f15c0..be2114d 100644 --- a/packages/schemas/package.json +++ b/packages/events/package.json @@ -1,30 +1,35 @@ { "author": "", "dependencies": { - "zod": "^4.0.5" + "zod": "^4.1.12" }, "description": "Schemas for NHS Notify configuration", "devDependencies": { - "@types/jest": "^30.0.0", + "@types/jest": "^29.5.14", "@types/node": "^22.13.4", - "jest": "^30.0.4", + "jest": "^29.7.0", "ts-jest": "^29.4.0", "ts-node": "^10.9.2", - "typescript": "^5.7.3" + "typescript": "^5.9.3", + "zod-mermaid": "^1.3.0" }, - "main": "src/index.ts", - "name": "@nhsdigital/nhs-notify-supplier-config-schemas", + "main": "dist/index.js", + "name": "@nhsdigital/nhs-notify-event-schemas-supplier-config", "publishConfig": { "registry": "https://npm.pkg.github.com" }, "repository": "git@github.com:NHSDigital/nhs-notify-supplier-config.git", "scripts": { - "build": "tsc", + "build": "tsc -p ./tsconfig.build.json", "dev": "ts-node src/index.ts", - "json": "ts-node src/cli/generate-json.ts", + "examples": "ts-node src/examples/specification-examples.ts", + "gen:erd": "ts-node src/cli/generate-erd.ts", + "gen:json": "ts-node src/cli/generate-json.ts", + "prepare": "npm run build", "start": "node dist/index.js", "test": "jest", "test:unit": "jest" }, + "types": "dist/index.d.ts", "version": "1.0.0" } diff --git a/packages/events/schemas/domain/.gitignore b/packages/events/schemas/domain/.gitignore new file mode 100644 index 0000000..8b5675c --- /dev/null +++ b/packages/events/schemas/domain/.gitignore @@ -0,0 +1 @@ +*.schema.json diff --git a/packages/events/schemas/events/.gitignore b/packages/events/schemas/events/.gitignore new file mode 100644 index 0000000..8b5675c --- /dev/null +++ b/packages/events/schemas/events/.gitignore @@ -0,0 +1 @@ +*.schema.json diff --git a/packages/events/src/cli/generate-erd.ts b/packages/events/src/cli/generate-erd.ts new file mode 100644 index 0000000..69594ec --- /dev/null +++ b/packages/events/src/cli/generate-erd.ts @@ -0,0 +1,57 @@ +// eslint-disable-next-line import-x/no-extraneous-dependencies +import { generateMermaidDiagram } from "zod-mermaid"; +import fs from "node:fs"; +import path from "node:path"; +import { z } from "zod"; +import { + $VolumeGroup, $Envelope, + $LetterVariant, + $PackSpecification, $Supplier, + $SupplierAllocation, + $SupplierPack, $Constraint, +} from "../domain"; + +// eslint-disable-next-line security/detect-non-literal-fs-filename +const out = fs.openSync(`${path.dirname(__filename)}/../domain/erd.md`, "w"); + +fs.writeSync( + out, + `# Event domain ERD + +This document contains the mermaid diagrams for the event domain model. + +The schemas are generated from Zod definitions and provide a visual representation of the data structure. +`, +); + +for (const [name, schema] of Object.entries({ + AllSchemas: [ + $LetterVariant, + $PackSpecification, + $VolumeGroup, + $Supplier, + $SupplierAllocation, + $SupplierPack, + $Envelope, + ], + LetterVariant: [$LetterVariant], + PackSpecification: [$PackSpecification, $Envelope], + SupplierAllocation: [$VolumeGroup, $Supplier, $SupplierAllocation], + SupplierPack: [$SupplierPack], +})) { + const mermaid = generateMermaidDiagram(schema); + fs.writeSync( + out, + ` +## ${name} schema + +${z.globalRegistry.get(schema[0])?.description} + +\`\`\`mermaid +${mermaid} +\`\`\` +`, + ); +} + +fs.closeSync(out); diff --git a/packages/events/src/cli/generate-json.ts b/packages/events/src/cli/generate-json.ts new file mode 100644 index 0000000..ca70c70 --- /dev/null +++ b/packages/events/src/cli/generate-json.ts @@ -0,0 +1,114 @@ +import { z } from "zod"; +import * as fs from "node:fs"; +import { + $LetterVariant, + $PackSpecification, + $Supplier, + $SupplierAllocation, + $SupplierPack, + $VolumeGroup, +} from "../domain"; +import { + $LetterVariantEvent, + letterVariantEvents, +} from "../events/letter-variant-events"; +import { + $PackSpecificationEvent, + packSpecificationEvents, +} from "../events/pack-specification-events"; +import { + $SupplierAllocationEvent, + supplierAllocationEvents, +} from "../events/supplier-allocation-events"; +import { + $SupplierPackEvent, + supplierPackEvents, +} from "../events/supplier-pack-events"; +import { $SupplierEvent, supplierEvents } from "../events/supplier-events"; +import { + $VolumeGroupEvent, + volumeGroupEvents, +} from "../events/volume-group-events"; + +/** + * Generate JSON schema for a single Zod schema and write to file + */ +function generateJsonSchema( + schema: z.ZodTypeAny, + outputPath: string, + schemaName: string, +): void { + const jsonSchema = z.toJSONSchema(schema, { + io: "input", + target: "openapi-3.0", + reused: "ref", + }); + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(outputPath, JSON.stringify(jsonSchema, null, 2)); + console.info(`Wrote JSON schema for ${schemaName} to ${outputPath}`); +} + +/** + * Generate JSON schemas for domain models + */ +function generateDomainSchemas(domainModels: Record) { + fs.mkdirSync("schemas/domain", { recursive: true }); + for (const [key, schema] of Object.entries(domainModels)) { + const outFile = `schemas/domain/${key}.schema.json`; + generateJsonSchema(schema, outFile, key); + } +} + +/** + * Generate JSON schemas for event types + */ +function generateEventSchemas(eventSchemas: Record) { + fs.mkdirSync("schemas/events", { recursive: true }); + for (const [key, schema] of Object.entries(eventSchemas)) { + const outFile = `schemas/events/${key}.schema.json`; + generateJsonSchema(schema, outFile, key); + } +} + +/** + * Generate a generic "any" event schema that matches any status for a given event type + */ +function generateAnyEventSchema(schema: z.ZodTypeAny, eventTypeName: string) { + fs.mkdirSync("schemas/events", { recursive: true }); + const outFile = `schemas/events/${eventTypeName}.any.schema.json`; + generateJsonSchema(schema, outFile, `${eventTypeName}.any`); +} + +// Generate domain schemas +generateDomainSchemas({ + "letter-variant": $LetterVariant, + "pack-specification": $PackSpecification, + "supplier-pack": $SupplierPack, + "volume-group": $VolumeGroup, + "supplier-allocation": $SupplierAllocation, + supplier: $Supplier, +}); + +// Generate event schemas for letter variants +generateEventSchemas(letterVariantEvents); +generateAnyEventSchema($LetterVariantEvent, "letter-variant"); + +// Generate event schemas for pack specifications +generateEventSchemas(packSpecificationEvents); +generateAnyEventSchema($PackSpecificationEvent, "pack-specification"); + +// Generate event schemas for supplier allocations +generateEventSchemas(supplierAllocationEvents); +generateAnyEventSchema($SupplierAllocationEvent, "supplier-allocation"); + +// Generate event schemas for supplier packs +generateEventSchemas(supplierPackEvents); +generateAnyEventSchema($SupplierPackEvent, "supplier-pack"); + +// Generate event schemas for suppliers +generateEventSchemas(supplierEvents); +generateAnyEventSchema($SupplierEvent, "supplier"); + +// Generate event schemas for volume groups +generateEventSchemas(volumeGroupEvents); +generateAnyEventSchema($VolumeGroupEvent, "volume-group"); diff --git a/packages/events/src/domain/__tests__/specification-supplier.test.ts b/packages/events/src/domain/__tests__/specification-supplier.test.ts new file mode 100644 index 0000000..3fc7038 --- /dev/null +++ b/packages/events/src/domain/__tests__/specification-supplier.test.ts @@ -0,0 +1,71 @@ +import { PackSpecification } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { $SupplierPack, SupplierPack } from "../supplier-pack"; + +describe("SpecificationSupplier schema validation", () => { + const standardLetterSpecification: PackSpecification = { + id: "standard-letter" as any, + name: "Standard Economy-class Letter", + status: "PROD", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + version: 1, + postage: { + id: "economy", + size: "STANDARD", + deliveryDays: 4, + }, + assembly: { + envelopeId: "nhs-economy", + printColour: "BLACK", + }, + }; + + const testSupplierPack: SupplierPack = { + id: "test-specification-supplier" as any, + packSpecificationId: standardLetterSpecification.id, + supplierId: "supplier-123", + approval: "APPROVED", + status: "PROD", + }; + + it("should validate a specification supplier", () => { + expect(() => $SupplierPack.parse(testSupplierPack)).not.toThrow(); + }); + + describe("approval status validation", () => { + it("should accept DRAFT approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "DRAFT" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should accept SUBMITTED approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "SUBMITTED" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should accept PROOF_RECEIVED approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "PROOF_RECEIVED" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should accept APPROVED approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "APPROVED" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should accept REJECTED approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "REJECTED" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should accept DISABLED approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "DISABLED" }; + expect(() => $SupplierPack.parse(supplierPack)).not.toThrow(); + }); + + it("should reject invalid approval status", () => { + const supplierPack = { ...testSupplierPack, approval: "INVALID_STATUS" }; + expect(() => $SupplierPack.parse(supplierPack)).toThrow(); + }); + }); +}); diff --git a/packages/events/src/domain/__tests__/specification.test.ts b/packages/events/src/domain/__tests__/specification.test.ts new file mode 100644 index 0000000..d70b8e4 --- /dev/null +++ b/packages/events/src/domain/__tests__/specification.test.ts @@ -0,0 +1,172 @@ +import { + $PackSpecification, + PackSpecification, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; + +describe("Specification schema validation", () => { + const standardLetterSpecification: PackSpecification = { + id: "standard-letter", + name: "Standard Economy-class Letter", + status: "INT", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + version: 1, + postage: { + id: "economy", + size: "STANDARD", + deliveryDays: 4, + }, + assembly: { + envelopeId: "nhs-economy", + printColour: "BLACK", + }, + }; + + it("should validate a standard letter specification", () => { + expect(() => + $PackSpecification.strict().parse(standardLetterSpecification), + ).not.toThrow(); + }); + + it("should accept a letter specification with unrecognised fields", () => { + expect(() => + $PackSpecification.parse({ + ...standardLetterSpecification, + additionalField: { some: "data" }, + }), + ).not.toThrow(); + }); + + it("should validate a specification with optional description", () => { + const specWithDescription: PackSpecification = { + ...standardLetterSpecification, + description: "A standard economy-class letter for bulk mailings", + }; + + expect(() => + $PackSpecification.strict().parse(specWithDescription), + ).not.toThrow(); + }); + + it("should validate a specification without description", () => { + const specWithoutDescription: PackSpecification = { + ...standardLetterSpecification, + }; + + expect(() => + $PackSpecification.strict().parse(specWithoutDescription), + ).not.toThrow(); + }); + + it("should validate a specification with duplex set to true", () => { + const specWithDuplex: PackSpecification = { + ...standardLetterSpecification, + assembly: { + ...standardLetterSpecification.assembly, + duplex: true, + }, + }; + + expect(() => + $PackSpecification.strict().parse(specWithDuplex), + ).not.toThrow(); + }); + + it("should validate a specification with duplex set to false", () => { + const specWithoutDuplex: PackSpecification = { + ...standardLetterSpecification, + assembly: { + ...standardLetterSpecification.assembly, + duplex: false, + }, + }; + + expect(() => + $PackSpecification.strict().parse(specWithoutDuplex), + ).not.toThrow(); + }); + + it("should validate a specification without duplex field", () => { + const specWithoutDuplexField: PackSpecification = { + ...standardLetterSpecification, + }; + + expect(() => + $PackSpecification.strict().parse(specWithoutDuplexField), + ).not.toThrow(); + }); + + it("should validate a specification with constraints", () => { + const specWithConstraints: PackSpecification = { + ...standardLetterSpecification, + constraints: { + sheets: { + value: 10, + operator: "LESS_THAN", + }, + deliveryDays: { + value: 4, + operator: "LESS_THAN", + }, + }, + }; + + expect(() => + $PackSpecification.strict().parse(specWithConstraints), + ).not.toThrow(); + }); + + it("should validate a specification with all constraint fields", () => { + const specWithConstraints: PackSpecification = { + ...standardLetterSpecification, + constraints: { + deliveryDays: { + value: 5, + operator: "EQUALS", + }, + sheets: { + value: 20, + operator: "LESS_THAN", + }, + sides: { + value: 4, + operator: "LESS_THAN", + }, + blackCoveragePercentage: { + value: 80, + operator: "LESS_THAN", + }, + colourCoveragePercentage: { + value: 50, + operator: "LESS_THAN", + }, + }, + }; + + expect(() => + $PackSpecification.strict().parse(specWithConstraints), + ).not.toThrow(); + }); + + it("should reject a specification with invalid constraint value", () => { + const specWithInvalidConstraints = { + ...standardLetterSpecification, + constraints: { + sheets: { + value: "not a number", + operator: "LESS_THAN", + }, + }, + }; + + expect(() => + $PackSpecification.strict().parse(specWithInvalidConstraints), + ).toThrow(); + }); + + it("should validate a specification without constraints", () => { + expect(() => + $PackSpecification.strict().parse(standardLetterSpecification), + ).not.toThrow(); + }); +}); diff --git a/packages/events/src/domain/channel.ts b/packages/events/src/domain/channel.ts new file mode 100644 index 0000000..5abc629 --- /dev/null +++ b/packages/events/src/domain/channel.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const $ChannelType = z.enum(["NHSAPP", "SMS", "EMAIL", "LETTER"]); + +export type ChannelType = z.infer; diff --git a/packages/events/src/domain/common.ts b/packages/events/src/domain/common.ts new file mode 100644 index 0000000..698d763 --- /dev/null +++ b/packages/events/src/domain/common.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const $EnvironmentStatus = z + .enum(["DRAFT", "INT", "PROD", "DISABLED"]) + .meta({ + title: "EnvironmentStatus", + description: + "Indicates whether the configuration is in draft, or enabled in the integration or production environment. " + + "`PROD` implies that the configuration is also enabled in the integration environment.", + }); diff --git a/packages/events/src/domain/constraint.ts b/packages/events/src/domain/constraint.ts new file mode 100644 index 0000000..2fa2473 --- /dev/null +++ b/packages/events/src/domain/constraint.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +export const $Constraint = z + .object({ + value: z.number(), + operator: z + .enum(["EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN"]) + .default("LESS_THAN"), + }) + .meta({ + title: "Constraint", + }); +export type Constraint = z.infer; + +export const $Constraints = z.object({ + deliveryDays: $Constraint.optional().meta({ + title: "Delivery Days", + description: + "The expected number of days for delivery under this configuration.", + }), + sheets: $Constraint.optional().meta({ + title: "Sheets of paper", + description: + "The number of sheets that can be accommodated with this configuration.", + }), + sides: $Constraint.optional().meta({ + title: "Sides", + description: + "The number of sides to be printed for this letter. Dependent on duplex printing options.", + }), + blackCoveragePercentage: $Constraint.optional().meta({ + title: "Black Coverage Percentage", + description: + "The percentage of black coverage allowed on the paper under this configuration.", + }), + colourCoveragePercentage: $Constraint.optional().meta({ + title: "Colour Coverage Percentage", + description: + "The percentage of colour coverage allowed on the paper under this configuration.", + }), +}); diff --git a/packages/events/src/domain/envelope.ts b/packages/events/src/domain/envelope.ts new file mode 100644 index 0000000..1a255fe --- /dev/null +++ b/packages/events/src/domain/envelope.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const $EnvelopeFeature = z.enum([ + "WHITEMAIL", + "NHS_BRANDING", + "NHS_BARCODE", +]); + +export const $Envelope = z + .object({ + id: z.string(), + name: z.string(), + size: z.enum(["C5", "C4", "DL"]), + features: z.array($EnvelopeFeature).optional(), + artwork: z.url().optional().meta({ + title: "Artwork URL", + description: + "An S3 URL pointing to the artwork for this envelope, if applicable.", + }), + maxThicknessMm: z + .number() + .optional() + .meta({ + title: "Max Thickness (mm)", + description: + "The maximum thickness in millimetres for this envelope. " + + "Used to validate that the assembled pack will fit within the envelope.", + }), + maxSheets: z + .number() + .optional() + .meta({ + title: "Max Sheets", + description: + "The maximum number of sheets that can be accommodated within this envelope. " + + "Used to validate that the assembled pack will fit within the envelope.", + }), + }) + .describe("Envelope"); +export type Envelope = z.infer; diff --git a/packages/events/src/domain/erd.md b/packages/events/src/domain/erd.md new file mode 100644 index 0000000..a75ee98 --- /dev/null +++ b/packages/events/src/domain/erd.md @@ -0,0 +1,249 @@ +# Event domain ERD + +This document contains the mermaid diagrams for the event domain model. + +The schemas are generated from Zod definitions and provide a visual representation of the data structure. + +## AllSchemas schema + +A Letter Variant describes a letter that can be produced with particular characteristics, and may be scoped to a single clientId and campaignId. + +```mermaid +erDiagram + LetterVariant { + string id + string name + string description + string type "enum: STANDARD, BRAILLE, AUDIO" + string status "enum: DRAFT, INT, PROD, DISABLED" + string volumeGroupId "ref: VolumeGroup" + string clientId + string[] campaignIds + string supplierId "ref: Supplier" + string[] packSpecificationIds "ref: PackSpecification" + Record constraints "<string, Constraints>" + } + PackSpecification { + string id + string name + string description + string status "enum: DRAFT, INT, PROD, DISABLED" + string createdAt + string updatedAt + number version "min: -9007199254740991, max: 9007199254740991" + string billingId + Record constraints "<string, Constraints>" + Postage postage + Assembly assembly + } + Postage { + string id + string size "enum: STANDARD, LARGE, PARCEL" + number deliveryDays + number maxWeightGrams + number maxThicknessMm + } + Assembly { + string envelopeId "ref: Envelope" + string printColour "enum: BLACK, COLOUR" + number blackCoveragePercentage + number colourCoveragePercentage + boolean duplex + Paper paper + string[] insertIds "ref: Insert" + string[] features "enum: BRAILLE, AUDIO, ADMAIL, SAME_DAY" + Record additional "<string, string>" + } + Paper { + string id + string name + number weightGSM + string size "enum: A5, A4, A3" + string colour "enum: WHITE" + string finish "enum: MATT, GLOSSY, SILK" + boolean recycled + } + VolumeGroup { + string id + string name + string description + string status "enum: DRAFT, INT, PROD, DISABLED" + string startDate + string endDate + } + Supplier { + string id + string name + string channelType "enum: NHSAPP, SMS, EMAIL, LETTER" + number dailyCapacity "min: -9007199254740991, max: 9007199254740991" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + SupplierAllocation { + string id + string volumeGroup "ref: VolumeGroup" + string supplier "ref: Supplier" + number allocationPercentage "positive, max: 100" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + SupplierPack { + string id + string packSpecificationId "ref: PackSpecification" + string supplierId "ref: Supplier" + string approval "enum: DRAFT, SUBMITTED, PROOF_RECEIVED, APPROVED, REJECTED, DISABLED" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + Envelope { + string id + string name + string size "enum: C5, C4, DL" + string[] features "enum: WHITEMAIL, NHS_BRANDING, NHS_BARCODE" + string artwork "url" + number maxThicknessMm + number maxSheets + } + LetterVariant }o--|| VolumeGroup : "volumeGroupId" + LetterVariant }o--o{ Supplier : "supplierId" + LetterVariant }o--o{ PackSpecification : "packSpecificationIds" + PackSpecification ||--|| Postage : "postage" + PackSpecification ||--o{ Assembly : "assembly" + Assembly }o--o{ Envelope : "envelopeId" + Assembly ||--o{ Paper : "paper" + SupplierAllocation }o--|| VolumeGroup : "volumeGroup" + SupplierAllocation }o--|| Supplier : "supplier" + SupplierPack }o--|| PackSpecification : "packSpecificationId" + SupplierPack }o--|| Supplier : "supplierId" +``` + +## LetterVariant schema + +A Letter Variant describes a letter that can be produced with particular characteristics, and may be scoped to a single clientId and campaignId. + +```mermaid +erDiagram + LetterVariant { + string id + string name + string description + string type "enum: STANDARD, BRAILLE, AUDIO" + string status "enum: DRAFT, INT, PROD, DISABLED" + string volumeGroupId "ref: VolumeGroup" + string clientId + string[] campaignIds + string supplierId "ref: Supplier" + string[] packSpecificationIds "ref: PackSpecification" + Record constraints "<string, Constraints>" + } + LetterVariant }o--|| VolumeGroup : "volumeGroupId" + LetterVariant }o--o{ Supplier : "supplierId" + LetterVariant }o--o{ PackSpecification : "packSpecificationIds" +``` + +## PackSpecification schema + +A PackSpecification defines the composition, postage and assembly attributes for producing a pack. + +```mermaid +erDiagram + PackSpecification { + string id + string name + string description + string status "enum: DRAFT, INT, PROD, DISABLED" + string createdAt + string updatedAt + number version "min: -9007199254740991, max: 9007199254740991" + string billingId + Record constraints "<string, Constraints>" + Postage postage + Assembly assembly + } + Postage { + string id + string size "enum: STANDARD, LARGE, PARCEL" + number deliveryDays + number maxWeightGrams + number maxThicknessMm + } + Assembly { + string envelopeId "ref: Envelope" + string printColour "enum: BLACK, COLOUR" + number blackCoveragePercentage + number colourCoveragePercentage + boolean duplex + Paper paper + string[] insertIds "ref: Insert" + string[] features "enum: BRAILLE, AUDIO, ADMAIL, SAME_DAY" + Record additional "<string, string>" + } + Paper { + string id + string name + number weightGSM + string size "enum: A5, A4, A3" + string colour "enum: WHITE" + string finish "enum: MATT, GLOSSY, SILK" + boolean recycled + } + Envelope { + string id + string name + string size "enum: C5, C4, DL" + string[] features "enum: WHITEMAIL, NHS_BRANDING, NHS_BARCODE" + string artwork "url" + number maxThicknessMm + number maxSheets + } + PackSpecification ||--|| Postage : "postage" + PackSpecification ||--o{ Assembly : "assembly" + Assembly }o--o{ Envelope : "envelopeId" + Assembly ||--o{ Paper : "paper" +``` + +## SupplierAllocation schema + +A volume group representing several lots within a competition framework under which suppliers will be allocated capacity. + +```mermaid +erDiagram + VolumeGroup { + string id + string name + string description + string status "enum: DRAFT, INT, PROD, DISABLED" + string startDate + string endDate + } + Supplier { + string id + string name + string channelType "enum: NHSAPP, SMS, EMAIL, LETTER" + number dailyCapacity "min: -9007199254740991, max: 9007199254740991" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + SupplierAllocation { + string id + string volumeGroup "ref: VolumeGroup" + string supplier "ref: Supplier" + number allocationPercentage "positive, max: 100" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + SupplierAllocation }o--|| VolumeGroup : "volumeGroup" + SupplierAllocation }o--|| Supplier : "supplier" +``` + +## SupplierPack schema + +Indicates that a supplier is capable of producing a specific pack specification. + +```mermaid +erDiagram + SupplierPack { + string id + string packSpecificationId "ref: PackSpecification" + string supplierId "ref: Supplier" + string approval "enum: DRAFT, SUBMITTED, PROOF_RECEIVED, APPROVED, REJECTED, DISABLED" + string status "enum: DRAFT, INT, PROD, DISABLED" + } + SupplierPack }o--|| PackSpecification : "packSpecificationId" + SupplierPack }o--|| Supplier : "supplierId" +``` diff --git a/packages/events/src/domain/index.ts b/packages/events/src/domain/index.ts new file mode 100644 index 0000000..c1eaa13 --- /dev/null +++ b/packages/events/src/domain/index.ts @@ -0,0 +1,13 @@ +export * from "./channel"; +export * from "./supplier"; +export * from "./common"; +export * from "./volume-group"; +export * from "./letter-variant"; +export * from "./pack-specification"; +export * from "./supplier-allocation"; +export * from "./supplier-pack"; +export * from "./postage"; +export * from "./envelope"; +export * from "./constraint"; +export * from "./insert"; +export * from "./paper"; diff --git a/packages/events/src/domain/insert.ts b/packages/events/src/domain/insert.ts new file mode 100644 index 0000000..155d882 --- /dev/null +++ b/packages/events/src/domain/insert.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const $Insert = z + .object({ + id: z.string(), + name: z.string(), + type: z.enum(["FLYER", "BOOKLET", "ATTACHMENT"]), + source: z.enum(["IN_HOUSE", "EXTERNAL"]), + artwork: z.url().optional().meta({ + title: "Artwork URL", + description: + "An S3 URL pointing to the artwork for this insert, if applicable.", + }), + weightGrams: z.number().optional().meta({ + title: "Weight (grams)", + description: + "The weight in grams of this insert. Used to calculate postage options.", + }), + }) + .describe("Insert"); +export type Insert = z.infer; diff --git a/packages/events/src/domain/letter-variant.ts b/packages/events/src/domain/letter-variant.ts new file mode 100644 index 0000000..531d362 --- /dev/null +++ b/packages/events/src/domain/letter-variant.ts @@ -0,0 +1,65 @@ +import { $EnvironmentStatus } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/common"; +import { idRef } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/helpers/id-ref"; +import { $PackSpecification } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { z } from "zod"; +import { $VolumeGroup } from "./volume-group"; +import { $Supplier } from "./supplier"; +import { $Constraint, $Constraints } from "./constraint"; + +export const $LetterType = z.enum(["STANDARD", "BRAILLE", "AUDIO"]); + +export const $LetterVariant = z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + type: $LetterType, + status: $EnvironmentStatus, + volumeGroupId: idRef($VolumeGroup), + clientId: z + .string() + .optional() + .meta({ + title: "Client ID", + description: + "The clientId this letter variant is scoped to, if applicable. If omitted, all client IDs" + + " are eligible to use this letter variant.", + }), + campaignIds: z + .array(z.string()) + .optional() + .meta({ + title: "Campaign IDs", + description: + "The campaignIds this letter variant is scoped to, if applicable. " + + "This is used to restrict a particular variant to specific campaigns " + + "without the need for bespoke specifications, for example individual admail campaigns.", + }), + supplierId: idRef($Supplier) + .optional() + .meta({ + title: "Supplier ID", + description: + "The supplierId this letter variant is scoped to, if applicable. " + + "This is used to restrict a particular variant to a single supplier, " + + "for example individual admail campaigns.", + }), + packSpecificationIds: z.array(idRef($PackSpecification)).nonempty().meta({ + title: "Pack Specifications", + description: + "The pack specifications eligible for production of this letter variant.", + }), + constraints: $Constraints.optional().meta({ + title: "LetterVariant Constraints", + description: + "Constraints that apply to this letter variant, aggregating those in the pack " + + "specifications where specified.", + }), + }) + .meta({ + title: "LetterVariant", + description: + "A Letter Variant describes a letter that can be produced with particular " + + "characteristics, and may be scoped to a single clientId and campaignId.", + }); +export type LetterVariant = z.infer; diff --git a/packages/events/src/domain/pack-specification.ts b/packages/events/src/domain/pack-specification.ts new file mode 100644 index 0000000..b43d2f4 --- /dev/null +++ b/packages/events/src/domain/pack-specification.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; +import { $EnvironmentStatus } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/common"; +import { idRef } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/helpers/id-ref"; +import { $Postage } from "./postage"; +import { $Envelope } from "./envelope"; +import { $Paper } from "./paper"; +import { $Insert } from "./insert"; +import {$Constraint, $Constraints} from "./constraint"; + +export const $PackFeature = z.enum(["BRAILLE", "AUDIO", "ADMAIL", "SAME_DAY"]); + +export const $PackSpecification = z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + status: $EnvironmentStatus, + createdAt: z.iso.datetime(), + updatedAt: z.iso.datetime(), + version: z.int().meta({ + title: "Version", + description: + "The version number of this Pack Specification, incremented with each update.", + }), + billingId: z.string().optional(), + constraints: $Constraints.optional(), + postage: $Postage, + assembly: z + .object({ + envelopeId: idRef($Envelope), + printColour: z.enum(["BLACK", "COLOUR"]), + duplex: z.boolean(), + paper: $Paper, + insertIds: z.array(idRef($Insert)), + features: z.array($PackFeature), + additional: z.record(z.string(), z.string()), + }) + .partial() + .optional(), + }) + .meta({ + title: "PackSpecification", + description: + "A PackSpecification defines the composition, postage and assembly attributes for producing a pack.", + }); +export type PackSpecification = z.infer; diff --git a/packages/events/src/domain/paper.ts b/packages/events/src/domain/paper.ts new file mode 100644 index 0000000..a166a26 --- /dev/null +++ b/packages/events/src/domain/paper.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const $Paper = z + .object({ + id: z.string(), + name: z.string(), + weightGSM: z.number(), + size: z.enum(["A5", "A4", "A3"]), + colour: z.enum(["WHITE"]).meta({ + title: "Colour", + description: + "The colour of the paper. Currently we only define WHITE paper, but this may be extended in future.", + }), + finish: z.enum(["MATT", "GLOSSY", "SILK"]).optional(), + recycled: z.boolean(), + }) + .describe("Paper"); +export type Paper = z.infer; diff --git a/packages/events/src/domain/postage.ts b/packages/events/src/domain/postage.ts new file mode 100644 index 0000000..1d7c504 --- /dev/null +++ b/packages/events/src/domain/postage.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +export const $Postage = z + .object({ + id: z.string(), + size: z.enum(["STANDARD", "LARGE", "PARCEL"]), + deliveryDays: z.number().optional().meta({ + title: "Delivery Days", + description: + "The expected number of days for delivery under this postage option.", + }), + maxWeightGrams: z + .number() + .optional() + .meta({ + title: "Max Weight (grams)", + description: + "The maximum weight in grams for this postage option. Places a " + + "constraint based on the number of sheets and paper weight.", + }), + maxThicknessMm: z + .number() + .optional() + .meta({ + title: "Max Thickness (mm)", + description: + "The maximum thickness in millimetres for this postage option. " + + "Places a constraint based on the number of sheets and paper type.", + }), + }) + .describe("Postage"); +export type Postage = z.infer; diff --git a/packages/events/src/domain/supplier-allocation.ts b/packages/events/src/domain/supplier-allocation.ts new file mode 100644 index 0000000..0c94860 --- /dev/null +++ b/packages/events/src/domain/supplier-allocation.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import { $EnvironmentStatus } from "./common"; +import { idRef } from "../helpers/id-ref"; +import { $VolumeGroup } from "./volume-group"; +import { $Supplier } from "./supplier"; + +export const $SupplierAllocation = z + .object({ + id: z.string(), + volumeGroup: idRef($VolumeGroup), + supplier: idRef($Supplier), + allocationPercentage: z.number().min(0).max(100), + status: $EnvironmentStatus, + }) + .meta({ + title: "SupplierAllocation", + description: + "A SupplierAllocation defines the proportion of the volume associated with a volume group which should be processed using a specific supplier.", + }); +export type SupplierAllocation = z.infer; diff --git a/packages/events/src/domain/supplier-pack.ts b/packages/events/src/domain/supplier-pack.ts new file mode 100644 index 0000000..a3123ae --- /dev/null +++ b/packages/events/src/domain/supplier-pack.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { idRef } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/helpers/id-ref"; +import { $PackSpecification } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { $EnvironmentStatus } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/common"; +import { $Supplier } from "./supplier"; + +export const $SupplierPack = z + .object({ + id: z.string(), + packSpecificationId: idRef($PackSpecification), + supplierId: idRef($Supplier), + approval: z + .enum([ + "DRAFT", + "SUBMITTED", + "PROOF_RECEIVED", + "APPROVED", + "REJECTED", + "DISABLED", + ]) + .meta({ + title: "Approval Status", + description: + "Indicates the current state of the supplier pack approval process.", + }), + status: $EnvironmentStatus, + }) + .meta({ + title: "SupplierPack", + description: + "Indicates that a supplier is capable of producing a specific pack specification.", + }); +export type SupplierPack = z.infer; diff --git a/packages/events/src/domain/supplier.ts b/packages/events/src/domain/supplier.ts new file mode 100644 index 0000000..3f37b72 --- /dev/null +++ b/packages/events/src/domain/supplier.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { $ChannelType } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/channel"; +import { $EnvironmentStatus } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/common"; + +export const $Supplier = z + .object({ + id: z.string(), + name: z.string(), + channelType: $ChannelType, + dailyCapacity: z.number().int(), + status: $EnvironmentStatus, + }) + .describe("Supplier"); + +export type Supplier = z.infer; diff --git a/packages/events/src/domain/volume-group.ts b/packages/events/src/domain/volume-group.ts new file mode 100644 index 0000000..15126b8 --- /dev/null +++ b/packages/events/src/domain/volume-group.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { $EnvironmentStatus } from "./common"; + +export const $VolumeGroup = z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + status: $EnvironmentStatus, + startDate: z.iso.date(), // ISO date + endDate: z.iso.date().optional(), // ISO date + }) + .meta({ + title: "VolumeGroup", + description: + "A volume group representing several lots within a competition framework under which suppliers will be allocated capacity.", + }); +export type VolumeGroup = z.infer; diff --git a/packages/events/src/events/__tests__/event-envelope.test.ts b/packages/events/src/events/__tests__/event-envelope.test.ts new file mode 100644 index 0000000..8f5e61d --- /dev/null +++ b/packages/events/src/events/__tests__/event-envelope.test.ts @@ -0,0 +1,254 @@ +import { z } from "zod"; +import { EventEnvelope } from "../event-envelope"; + +describe("EventEnvelope schema validation", () => { + const $Envelope = EventEnvelope("order.read", "order", z.any(), ["READ"]); + type Envelope = z.infer; + + const baseValidEnvelope: Envelope = { + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/order.read.1.0.0.schema.json", + specversion: "1.0", + id: "6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111", + source: "/control-plane/supplier-config/ordering", + subject: "order/769acdd4", + type: "uk.nhs.notify.supplier-config.order.read.v1", + plane: "control", + time: "2025-10-01T10:15:30.000Z", + dataschemaversion: "1.0.0", + data: { + "notify-payload": { + "notify-data": { nhsNumber: "9434765919" }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Ordering", + version: "1.3.0", + }, + }, + }, + traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + recordedtime: "2025-10-01T10:15:30.250Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + }; + + describe("basic validation", () => { + it("should validate a valid envelope", () => { + const result = $Envelope.safeParse(baseValidEnvelope); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should validate control-plane source", () => { + const envelope: Envelope = { + ...baseValidEnvelope, + source: "/data-plane/supplier-config/ordering", + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + }); + + describe("superRefine: severity text and number validation", () => { + it("should accept TRACE with severitynumber 0", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "TRACE", + severitynumber: 0, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should accept DEBUG with severitynumber 1", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "DEBUG", + severitynumber: 1, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should accept INFO with severitynumber 2", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "INFO", + severitynumber: 2, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should accept WARN with severitynumber 3", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "WARN", + severitynumber: 3, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should accept ERROR with severitynumber 4", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "ERROR", + severitynumber: 4, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should accept FATAL with severitynumber 5", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "FATAL", + severitynumber: 5, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + it("should reject TRACE with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "TRACE", + severitynumber: 1, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject DEBUG with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "DEBUG", + severitynumber: 2, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject INFO with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "INFO", + severitynumber: 1, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject WARN with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "WARN", + severitynumber: 2, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject ERROR with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "ERROR", + severitynumber: 3, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject FATAL with incorrect severitynumber", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "FATAL", + severitynumber: 4, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should reject severitynumber without severitytext", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: undefined, + severitynumber: 2, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + + it("should accept severitytext without severitynumber (optional)", () => { + const envelope = { + ...baseValidEnvelope, + severitytext: "INFO", + severitynumber: 2, + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + }); + + describe("optional fields validation", () => { + it("should accept envelope with all optional fields", () => { + const envelope = { + ...baseValidEnvelope, + datacontenttype: "application/json", + tracestate: "rojo=00f067aa0ba902b7", + partitionkey: "customer-920fca11", + sampledrate: 5, + sequence: "00000000000000000042", + severitytext: "DEBUG", + severitynumber: 1, + dataclassification: "restricted", + dataregulation: "GDPR", + datacategory: "sensitive", + }; + + const result = $Envelope.safeParse(envelope); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should handle control-plane source with multiple path segments", () => { + const envelope = { + ...baseValidEnvelope, + source: "/control-plane/supplier-config/security", + }; + + const result = $Envelope.safeParse(envelope); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should reject invalid source pattern", () => { + const envelope = { + ...baseValidEnvelope, + source: "/invalid-plane/test", + }; + + const result = $Envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + }); +}); diff --git a/packages/events/src/events/__tests__/letter-variant-events.test.ts b/packages/events/src/events/__tests__/letter-variant-events.test.ts new file mode 100644 index 0000000..faf0e44 --- /dev/null +++ b/packages/events/src/events/__tests__/letter-variant-events.test.ts @@ -0,0 +1,404 @@ +import { + $LetterVariantEvent, + LetterVariantEvent, + letterVariantEvents, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/letter-variant-events"; + +describe("LetterVariant Events", () => { + describe("letter-variant.prod event", () => { + const validProdEvent: LetterVariantEvent = { + specversion: "1.0", + id: "6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111", + source: "/control-plane/supplier-config", + subject: "letter-variant/standard-letter-variant", + type: "uk.nhs.notify.supplier-config.letter-variant.prod.v1", + plane: "control", + time: "2025-10-01T10:15:30.000Z", + recordedtime: "2025-10-01T10:15:30.250Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.prod.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + data: { + id: "standard-letter-variant", + name: "Standard Letter Variant", + description: "A standard letter variant for general correspondence", + volumeGroupId: "supplier-framework-123", + type: "STANDARD", + status: "PROD", + packSpecificationIds: ["bau-standard-c5", "bau-standard-c4"], + }, + }; + + it("should validate a valid letter-variant.prod event", () => { + const result = $LetterVariantEvent.safeParse(validProdEvent); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should validate using the specialised published schema", () => { + const prodSchema = letterVariantEvents["letter-variant.prod"]; + const result = prodSchema.safeParse(validProdEvent); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should validate event with optional fields", () => { + const eventWithOptionalFields: LetterVariantEvent = { + ...validProdEvent, + data: { + ...validProdEvent.data, + clientId: "client-123", + campaignIds: ["campaign-456", "campaign-789"], + }, + }; + + const result = $LetterVariantEvent.safeParse(eventWithOptionalFields); + expect(result.success).toBe(true); + }); + + it("should validate BRAILLE type variant", () => { + const brailleEvent: LetterVariantEvent = { + ...validProdEvent, + data: { + id: "braille-variant", + name: "Braille Letter Variant", + volumeGroupId: "supplier-framework-123", + type: "BRAILLE", + status: "PROD", + packSpecificationIds: ["braille"], + }, + }; + + const result = $LetterVariantEvent.safeParse(brailleEvent); + expect(result.success).toBe(true); + }); + + it("should validate AUDIO type variant", () => { + const audioEvent: LetterVariantEvent = { + ...validProdEvent, + data: { + id: "audio-variant", + name: "Audio Letter Variant", + volumeGroupId: "supplier-framework-123", + type: "AUDIO", + status: "PROD", + packSpecificationIds: ["audio"], + }, + }; + + const result = $LetterVariantEvent.safeParse(audioEvent); + expect(result.success).toBe(true); + }); + + it("should reject event with invalid type", () => { + const invalidEvent = { + ...validProdEvent, + type: "uk.nhs.notify.supplier-config.letter-variant.invalid.v1", + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with invalid source", () => { + const invalidEvent = { + ...validProdEvent, + source: "/data-plane/invalid", + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with invalid subject", () => { + const invalidEvent = { + ...validProdEvent, + subject: "invalid/subject", + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with mismatched dataschema using specialized schema", () => { + const prodSchema = letterVariantEvents["letter-variant.prod"]; + const invalidEvent = { + ...validProdEvent, + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.int.1.0.0.schema.json", + }; + + const result = prodSchema.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with invalid dataschema version format", () => { + const invalidEvent = { + ...validProdEvent, + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.int.2.0.0.schema.json", // Major version must be 1 + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with empty packSpecificationIds", () => { + const invalidEvent = { + ...validProdEvent, + data: { + ...validProdEvent.data, + packSpecificationIds: [], + }, + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should reject event with invalid letter type", () => { + const invalidEvent = { + ...validProdEvent, + data: { + ...validProdEvent.data, + type: "INVALID_TYPE", + }, + }; + + const result = $LetterVariantEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("should validate specialised schema enforces PROD status", () => { + const prodSchema = letterVariantEvents["letter-variant.prod"]; + + // Valid with PROD status + const validResult = prodSchema.safeParse(validProdEvent); + expect(validResult.success).toBe(true); + + // Invalid with INT status + const invalidEvent = { + ...validProdEvent, + data: { + ...validProdEvent.data, + status: "INT", + }, + }; + const invalidResult = prodSchema.safeParse(invalidEvent); + expect(invalidResult.success).toBe(false); + }); + + it("should validate letter variant with constraints", () => { + const eventWithConstraints = { + ...validProdEvent, + data: { + ...validProdEvent.data, + constraints: { + sheets: { + value: 10, + operator: "LESS_THAN", + }, + deliveryDays: { + value: 3, + operator: "LESS_THAN", + }, + }, + }, + }; + + const result = $LetterVariantEvent.safeParse(eventWithConstraints); + expect(result.success).toBe(true); + }); + + it("should validate letter variant with all constraint fields", () => { + const eventWithConstraints = { + ...validProdEvent, + data: { + ...validProdEvent.data, + constraints: { + deliveryDays: { + value: 5, + operator: "EQUALS", + }, + sheets: { + value: 20, + operator: "LESS_THAN", + }, + blackCoveragePercentage: { + value: 80, + operator: "LESS_THAN", + }, + colourCoveragePercentage: { + value: 50, + operator: "LESS_THAN", + }, + }, + }, + }; + + const result = $LetterVariantEvent.safeParse(eventWithConstraints); + expect(result.success).toBe(true); + }); + + it("should reject letter variant with invalid constraint field", () => { + const eventWithInvalidConstraints = { + ...validProdEvent, + data: { + ...validProdEvent.data, + constraints: { + sheets: { + value: "not a number", + operator: "LESS_THAN", + }, + }, + }, + }; + + const result = $LetterVariantEvent.safeParse(eventWithInvalidConstraints); + expect(result.success).toBe(false); + }); + + it("should validate letter variant without constraints", () => { + const result = $LetterVariantEvent.safeParse(validProdEvent); + expect(result.success).toBe(true); + }); + }); + + describe("letter-variant.int event", () => { + const validIntEvent: LetterVariantEvent = { + specversion: "1.0", + id: "7f2d3b64-4e65-4b1b-8c1c-bf3e5d222abc", + source: "/control-plane/supplier-config", + subject: "letter-variant/disabled-letter-variant", + type: "uk.nhs.notify.supplier-config.letter-variant.int.v1", + plane: "control", + time: "2025-10-01T11:20:45.000Z", + recordedtime: "2025-10-01T11:20:45.500Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.int.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-1bf8762027de54ee9559fc322d91430d-c8be7c2270314442-01", + data: { + id: "disabled-letter-variant", + name: "Disabled Letter Variant", + volumeGroupId: "supplier-framework-123", + type: "STANDARD", + status: "INT", + packSpecificationIds: ["bau-standard-c5"], + }, + }; + + it("should validate a valid letter-variant.int event", () => { + const result = $LetterVariantEvent.safeParse(validIntEvent); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should validate using the specialised disabled schema", () => { + const intSchema = letterVariantEvents["letter-variant.int"]; + const result = intSchema.safeParse(validIntEvent); + expect(result.success).toBe(true); + }); + + it("should validate specialised schema enforces INT status", () => { + const intSchema = letterVariantEvents["letter-variant.int"]; + + // Valid with INT status + const validResult = intSchema.safeParse(validIntEvent); + expect(validResult.success).toBe(true); + + // Invalid with PROD status + const invalidEvent = { + ...validIntEvent, + data: { + ...validIntEvent.data, + status: "PROD", + }, + }; + const invalidResult = intSchema.safeParse(invalidEvent); + expect(invalidResult.success).toBe(false); + }); + }); + + describe("letter-variant.disabled event", () => { + const validDisabledEvent: LetterVariantEvent = { + specversion: "1.0", + id: "8f3e4c75-5f76-5c2c-9d2d-cf4f6e333bcd", + source: "/control-plane/supplier-config", + subject: "letter-variant/disabled-letter-variant", + type: "uk.nhs.notify.supplier-config.letter-variant.disabled.v1", + plane: "control", + time: "2025-10-01T12:25:00.000Z", + recordedtime: "2025-10-01T12:25:00.750Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/letter-variant.disabled.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-2cf9873138ef65ff0670fd433e02541e-d9cf8d3381425553-01", + data: { + id: "disabled-letter-variant", + name: "Disabled Letter Variant", + description: "A letter variant that has been disabled", + volumeGroupId: "supplier-framework-123", + type: "STANDARD", + status: "DISABLED", + packSpecificationIds: ["bau-standard-c5"], + }, + }; + + it("should validate a valid letter-variant.disabled event", () => { + const result = $LetterVariantEvent.safeParse(validDisabledEvent); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + }); + + it("should validate using the specialised disabled schema", () => { + const disabledSchema = letterVariantEvents["letter-variant.disabled"]; + const result = disabledSchema.safeParse(validDisabledEvent); + expect(result.success).toBe(true); + }); + + it("should validate specialised schema enforces DISABLED status", () => { + const disabledSchema = letterVariantEvents["letter-variant.disabled"]; + + // Valid with DISABLED status + const validResult = disabledSchema.safeParse(validDisabledEvent); + expect(validResult.success).toBe(true); + + // Invalid with PROD status + const invalidEvent = { + ...validDisabledEvent, + data: { + ...validDisabledEvent.data, + status: "PROD", + }, + }; + const invalidResult = disabledSchema.safeParse(invalidEvent); + expect(invalidResult.success).toBe(false); + }); + }); + + describe("letterVariantEvents object", () => { + it("should contain draft, int, prod and disabled event schemas", () => { + expect(letterVariantEvents["letter-variant.draft"]).toBeDefined(); + expect(letterVariantEvents["letter-variant.int"]).toBeDefined(); + expect(letterVariantEvents["letter-variant.prod"]).toBeDefined(); + expect(letterVariantEvents["letter-variant.disabled"]).toBeDefined(); + }); + + it("should not contain published event schema", () => { + expect( + (letterVariantEvents as any)["letter-variant.published"], + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/events/src/events/__tests__/supplier-events.test.ts b/packages/events/src/events/__tests__/supplier-events.test.ts new file mode 100644 index 0000000..0b67ff5 --- /dev/null +++ b/packages/events/src/events/__tests__/supplier-events.test.ts @@ -0,0 +1,172 @@ +import { + $SupplierEvent, + supplierEvents, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/events/supplier-events"; + +describe("Supplier Events", () => { + describe("supplier.prod event", () => { + const validProdEvent = { + specversion: "1.0", + id: "6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111", + source: "/control-plane/supplier-config", + subject: "supplier/test-supplier", + type: "uk.nhs.notify.supplier-config.supplier.prod.v1", + plane: "control", + time: "2025-10-01T10:15:30.000Z", + recordedtime: "2025-10-01T10:15:30.250Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier.prod.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + data: { + id: "test-supplier", + name: "Test Supplier", + channelType: "LETTER", + dailyCapacity: 10_000, + status: "PROD", + }, + }; + + it("validates a supplier.prod event with generic schema", () => { + const result = $SupplierEvent.safeParse(validProdEvent); + expect(result.success).toBe(true); + }); + + it("validates with specialised prod schema", () => { + const prodSchema = supplierEvents["supplier.prod"]; + const result = prodSchema.safeParse(validProdEvent); + expect(result.success).toBe(true); + }); + + it("rejects mismatched dataschema using specialised schema", () => { + const prodSchema = supplierEvents["supplier.prod"]; + const invalidEvent = { + ...validProdEvent, + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier.int.1.0.0.schema.json", + }; + const result = prodSchema.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + + it("rejects invalid subject", () => { + const invalidEvent = { + ...validProdEvent, + subject: "supplier/INVALID SUBJ", + }; + const result = $SupplierEvent.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + }); + + describe("supplier.int event", () => { + const validIntEvent = { + specversion: "1.0", + id: "7f2d3b64-4e65-4b1b-8c1c-bf3e5d222abc", + source: "/control-plane/supplier-config", + subject: "supplier/int-supplier", + type: "uk.nhs.notify.supplier-config.supplier.int.v1", + plane: "control", + time: "2025-10-01T11:20:45.000Z", + recordedtime: "2025-10-01T11:20:45.500Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier.int.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-1bf8762027de54ee9559fc322d91430d-c8be7c2270314442-01", + data: { + id: "int-supplier", + name: "Integration Supplier", + channelType: "LETTER", + dailyCapacity: 5000, + status: "INT", + }, + }; + + it("validates a supplier.int event with generic schema", () => { + const result = $SupplierEvent.safeParse(validIntEvent); + expect(result.success).toBe(true); + }); + + it("validates with specialised int schema", () => { + const intSchema = supplierEvents["supplier.int"]; + const result = intSchema.safeParse(validIntEvent); + expect(result.success).toBe(true); + }); + + it("rejects event with PROD status using int schema", () => { + const intSchema = supplierEvents["supplier.int"]; + const invalidEvent = { + ...validIntEvent, + data: { ...validIntEvent.data, status: "PROD" }, + }; + const result = intSchema.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + }); + + describe("supplier.disabled event", () => { + const validDisabledEvent = { + specversion: "1.0", + id: "8f3e4c75-5f76-5c2c-9d2d-cf4f6e333bcd", + source: "/control-plane/supplier-config", + subject: "supplier/disabled-supplier", + type: "uk.nhs.notify.supplier-config.supplier.disabled.v1", + plane: "control", + time: "2025-10-01T12:25:00.000Z", + recordedtime: "2025-10-01T12:25:00.750Z", + severitynumber: 2, + severitytext: "INFO", + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-config/supplier.disabled.1.0.0.schema.json", + dataschemaversion: "1.0.0", + traceparent: "00-2cf9873138ef65ff0670fd433e02541e-d9cf8d3381425553-01", + data: { + id: "disabled-supplier", + name: "Disabled Supplier", + channelType: "LETTER", + dailyCapacity: 3000, + status: "DISABLED", + }, + }; + + it("validates a supplier.disabled event with generic schema", () => { + const result = $SupplierEvent.safeParse(validDisabledEvent); + expect(result.success).toBe(true); + }); + + it("validates with specialised disabled schema", () => { + const disabledSchema = supplierEvents["supplier.disabled"]; + const result = disabledSchema.safeParse(validDisabledEvent); + expect(result.success).toBe(true); + }); + + it("rejects event with PROD status using disabled schema", () => { + const disabledSchema = supplierEvents["supplier.disabled"]; + const invalidEvent = { + ...validDisabledEvent, + data: { ...validDisabledEvent.data, status: "PROD" }, + }; + const result = disabledSchema.safeParse(invalidEvent); + expect(result.success).toBe(false); + }); + }); + + describe("supplierEvents object", () => { + it("contains draft, int, prod and disabled event schemas", () => { + expect(supplierEvents["supplier.draft"]).toBeDefined(); + expect(supplierEvents["supplier.int"]).toBeDefined(); + expect(supplierEvents["supplier.prod"]).toBeDefined(); + expect(supplierEvents["supplier.disabled"]).toBeDefined(); + }); + it("does not contain published event schema", () => { + expect((supplierEvents as any)["supplier.published"]).toBeUndefined(); + }); + }); +}); diff --git a/packages/events/src/events/event-envelope.ts b/packages/events/src/events/event-envelope.ts new file mode 100644 index 0000000..b81899e --- /dev/null +++ b/packages/events/src/events/event-envelope.ts @@ -0,0 +1,258 @@ +import { z } from "zod"; + +// eslint-disable-next-line import-x/prefer-default-export +export function EventEnvelope( + eventName: string, + resourceName: string, + data: TData, + statuses: readonly string[], +) { + const statusRegex = statuses.map((s) => s.toLowerCase()).join("|"); + + // Pre-compute type strings to avoid repeated inference + const typeStrings = statuses.map( + (status) => + `uk.nhs.notify.supplier-config.${resourceName}.${status.toLowerCase()}.v1` as const, + ); + + const schemaExamples = statuses.map( + (status) => + `https://notify.nhs.uk/cloudevents/schemas/supplier-config/${resourceName}.${status.toLowerCase()}.1.0.0.schema.json`, + ); + + return z + .object({ + specversion: z.literal("1.0").meta({ + title: "CloudEvents spec version", + description: "CloudEvents specification version (fixed to 1.0).", + examples: ["1.0"], + }), + + id: z + .uuid() + .min(1) + .meta({ + title: "Event ID", + description: "Unique identifier for this event instance (UUID).", + examples: ["6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111"], + }), + + type: z.enum(typeStrings as [string, ...string[]]).meta({ + title: `${eventName} event type`, + description: "Event type using reverse-DNS style", + examples: typeStrings, + }), + + plane: z.literal("control").meta({ + title: "plane", + description: "The event bus that this event will be published to", + examples: ["control"], + }), + + dataschema: z + .string() + .regex( + // eslint-disable-next-line security/detect-non-literal-regexp + new RegExp( + `^https://notify\\.nhs\\.uk/cloudevents/schemas/supplier-config/${resourceName}\\.(?${statusRegex})\\.1\\.\\d+\\.\\d+\\.schema.json$`, + ), + ) + .meta({ + title: "Data Schema URI", + description: `URI of a schema that describes the event data\n\nData schema version must match the major version indicated by the type`, + examples: schemaExamples, + }), + + dataschemaversion: z + .string() + .regex(/^1\.\d+\.\d+$/) + .meta({ + title: "Data Schema URI", + description: `Version of the schema that describes the event data\n\nMust match the version in dataschema`, + examples: ["1.0.0"], + }), + + source: z + .string() + .regex(/^\/control-plane\/supplier-config(?:\/.*)?$/) + .meta({ + title: "Event Source", + description: + "Logical event producer path within the supplier-config domain", + }), + + subject: z + .string() + // eslint-disable-next-line security/detect-non-literal-regexp + .regex(new RegExp(`^${resourceName}/[a-z0-9-]+$`)) + .meta({ + title: "Event Subject", + description: + "Resource path (no leading slash) within the source made of segments separated by '/'.", + examples: [ + "pack-specification/f47ac10b-58cc-4372-a567-0e02b2c3d479", + ], + }), + + data, + + time: z.iso.datetime().meta({ + title: "Event Time", + description: "Timestamp when the event occurred (RFC 3339).", + examples: ["2025-10-01T10:15:30.000Z"], + }), + + datacontenttype: z.literal("application/json").meta({ + title: "Data Content Type", + description: + "Media type for the data field (fixed to application/json).", + examples: ["application/json"], + }), + + traceparent: z + .string() + .min(1) + .regex(/^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/) + .meta({ + title: "Traceparent", + description: "W3C Trace Context traceparent header value.", + examples: ["00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"], + }), + + tracestate: z.optional( + z.string().meta({ + title: "Tracestate", + description: "Optional W3C Trace Context tracestate header value.", + examples: ["rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"], + }), + ), + + partitionkey: z.optional( + z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9-]+$/) + .meta({ + title: "Partition Key", + description: + "Partition / ordering key (lowercase alphanumerics and hyphen, 1-64 chars).", + examples: ["customer-920fca11"], + }), + ), + + recordedtime: z.iso.datetime().meta({ + title: "Recorded Time", + description: + "Timestamp when the event was recorded/persisted (should be >= time).", + examples: ["2025-10-01T10:15:30.250Z"], + }), + + sampledrate: z.optional( + z + .number() + .int() + .min(1) + .meta({ + title: "Sampled Rate", + description: + "Sampling factor: number of similar occurrences this event represents.", + examples: [5], + }), + ), + + sequence: z.optional( + z + .string() + .regex(/^\d{20}$/) + .meta({ + title: "Sequence", + description: + "Zero-padded 20 digit numeric sequence (lexicographically sortable).", + examples: ["00000000000000000042"], + }), + ), + + severitytext: z.optional( + z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"]).meta({ + title: "Severity Text", + description: "Log severity level name.", + examples: ["DEBUG"], + }), + ), + + severitynumber: z + .number() + .int() + .min(0) + .max(5) + .meta({ + title: "Severity Number", + description: + "Numeric severity (TRACE=0, DEBUG=1, INFO=2, WARN=3, ERROR=4, FATAL=5).", + examples: [1], + }), + + dataclassification: z.optional( + z.enum(["public", "internal", "confidential", "restricted"]).meta({ + title: "Data Classification", + description: "Data sensitivity classification.", + examples: ["restricted"], + }), + ), + + dataregulation: z.optional( + z + .enum([ + "GDPR", + "HIPAA", + "PCI-DSS", + "ISO-27001", + "NIST-800-53", + "CCPA", + ]) + .meta({ + title: "Data Regulation", + description: "Regulatory regime tag applied to this data.", + examples: ["ISO-27001"], + }), + ), + + datacategory: z.optional( + z + .enum(["non-sensitive", "standard", "sensitive", "special-category"]) + .meta({ + title: "Data Category", + description: + "Data category classification (e.g. standard, special-category).", + examples: ["sensitive"], + }), + ), + }) + .superRefine((obj, ctx) => { + if (obj.severitytext !== undefined) { + const mapping = { + TRACE: 0, + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + FATAL: 5, + }; + if (obj.severitynumber !== mapping[obj.severitytext]) { + ctx.addIssue({ + code: "custom", + message: `severitynumber must be ${mapping[obj.severitytext]} when severitytext is ${obj.severitytext}`, + path: ["severitynumber"], + }); + } + } + if (obj.severitynumber && obj.severitytext === undefined) { + ctx.addIssue({ + code: "custom", + message: "severitytext is required when severitynumber is present", + path: ["severitytext"], + }); + } + }); +} diff --git a/packages/events/src/events/letter-variant-events.ts b/packages/events/src/events/letter-variant-events.ts new file mode 100644 index 0000000..59e44ff --- /dev/null +++ b/packages/events/src/events/letter-variant-events.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; +import { + $LetterVariant, + LetterVariant, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/letter-variant"; +import { EventEnvelope } from "./event-envelope"; + +const variantStatuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly LetterVariant["status"][]; + +/** + * A generic schema for parsing any letter status change event + */ +export const $LetterVariantEvent = EventEnvelope( + "letter-variant", + "letter-variant", + $LetterVariant, + variantStatuses, +).meta({ + title: `letter-variant.* Event`, + description: `Generic event schema for letter variant changes`, +}); + +export type LetterVariantEvent = z.infer; + +/** + * Specialise the generic event schema for a single status + * @param status + */ +const specialiseLetterVariantEvent = (status: LetterVariant["status"]) => { + return EventEnvelope( + `letter-variant.${status.toLowerCase()}`, + "letter-variant", + $LetterVariant + .extend({ + status: z.literal(status), + }) + .meta({ + description: `The status of a LetterVariant indicates whether it is available for use. + +For this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `letter-variant.${status.toLowerCase()} Event`, + description: `Event schema for letter variant change to ${status}`, + }); +}; + +export const letterVariantEvents = { + "letter-variant.draft": specialiseLetterVariantEvent("DRAFT"), + "letter-variant.int": specialiseLetterVariantEvent("INT"), + "letter-variant.prod": specialiseLetterVariantEvent("PROD"), + "letter-variant.disabled": specialiseLetterVariantEvent("DISABLED"), +} as const; diff --git a/packages/events/src/events/pack-specification-events.ts b/packages/events/src/events/pack-specification-events.ts new file mode 100644 index 0000000..5798b98 --- /dev/null +++ b/packages/events/src/events/pack-specification-events.ts @@ -0,0 +1,56 @@ +import { + $PackSpecification, + PackSpecification, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { z } from "zod"; +import { EventEnvelope } from "./event-envelope"; + +const packStatuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly PackSpecification["status"][]; + +/** + * Generic schema for parsing any PackSpecification status change event + */ +export const $PackSpecificationEvent = EventEnvelope( + "pack-specification", + "pack-specification", + $PackSpecification, + packStatuses, +).meta({ + title: "pack-specification.* Event", + description: "Generic event schema for pack specification changes", +}); + +function specialisePackSpecificationEvent( + status: (typeof packStatuses)[number], +) { + const lcStatus = status.toLowerCase(); + return EventEnvelope( + `pack-specification.${lcStatus}`, + "pack-specification", + $PackSpecification + .extend({ + status: z.literal(status), + }) + .meta({ + description: `Indicates the current state of the pack specification. + +For this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `pack-specification.${lcStatus} Event`, + description: `Event schema for pack specification change to ${status}`, + }); +} + +export const packSpecificationEvents = { + "pack-specification.draft": specialisePackSpecificationEvent("DRAFT"), + "pack-specification.int": specialisePackSpecificationEvent("INT"), + "pack-specification.prod": specialisePackSpecificationEvent("PROD"), + "pack-specification.disabled": specialisePackSpecificationEvent("DISABLED"), +} as const; diff --git a/packages/events/src/events/supplier-allocation-events.ts b/packages/events/src/events/supplier-allocation-events.ts new file mode 100644 index 0000000..b7b4d3f --- /dev/null +++ b/packages/events/src/events/supplier-allocation-events.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { $SupplierAllocation, SupplierAllocation } from "../domain"; +import { EventEnvelope } from "./event-envelope"; + +const allocationStatuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly SupplierAllocation["status"][]; + +/** + * Generic schema for parsing any SupplierAllocation status change event + */ +export const $SupplierAllocationEvent = EventEnvelope( + "supplier-allocation", + "supplier-allocation", + $SupplierAllocation, + allocationStatuses, +).meta({ + title: "supplier-allocation.* Event", + description: "Generic event schema for supplier allocation changes", +}); + +/** + * Specialise the generic event schema for a single status + * @param status + */ +function specialiseSupplierAllocationEvent( + status: (typeof allocationStatuses)[number], +) { + const lcStatus = status.toLowerCase(); + return EventEnvelope( + `supplier-allocation.${lcStatus}`, + "supplier-allocation", + $SupplierAllocation + .extend({ + status: z.literal(status), + }) + .meta({ + description: `Indicates the environment status of this supplier allocation. + +For this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `supplier-allocation.${lcStatus} Event`, + description: `Event schema for supplier allocation change to ${status}`, + }); +} + +export const supplierAllocationEvents = { + "supplier-allocation.draft": specialiseSupplierAllocationEvent("DRAFT"), + "supplier-allocation.int": specialiseSupplierAllocationEvent("INT"), + "supplier-allocation.prod": specialiseSupplierAllocationEvent("PROD"), + "supplier-allocation.disabled": specialiseSupplierAllocationEvent("DISABLED"), +} as const; diff --git a/packages/events/src/events/supplier-events.ts b/packages/events/src/events/supplier-events.ts new file mode 100644 index 0000000..f9060a6 --- /dev/null +++ b/packages/events/src/events/supplier-events.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { $Supplier, Supplier } from "../domain"; +import { EventEnvelope } from "./event-envelope"; + +const supplierStatuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly Supplier["status"][]; + +/** + * Generic schema for parsing any Supplier status change event + */ +export const $SupplierEvent = EventEnvelope( + "supplier", + "supplier", + $Supplier, + supplierStatuses, +).meta({ + title: "supplier.* Event", + description: "Generic event schema for supplier changes", +}); + +function specialiseSupplierEvent(status: (typeof supplierStatuses)[number]) { + const lcStatus = status.toLowerCase(); + return EventEnvelope( + `supplier.${lcStatus}`, + "supplier", + $Supplier + .extend({ + status: z.literal(status), + }) + .meta({ + description: `Indicates the current operational state of the supplier.\n\nFor this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `supplier.${lcStatus} Event`, + description: `Event schema for supplier change to ${status}`, + }); +} + +export const supplierEvents = { + "supplier.draft": specialiseSupplierEvent("DRAFT"), + "supplier.int": specialiseSupplierEvent("INT"), + "supplier.prod": specialiseSupplierEvent("PROD"), + "supplier.disabled": specialiseSupplierEvent("DISABLED"), +} as const; diff --git a/packages/events/src/events/supplier-pack-events.ts b/packages/events/src/events/supplier-pack-events.ts new file mode 100644 index 0000000..bf6d779 --- /dev/null +++ b/packages/events/src/events/supplier-pack-events.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { $SupplierPack, SupplierPack } from "../domain"; +import { EventEnvelope } from "./event-envelope"; + +const packStatuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly SupplierPack["status"][]; + +/** + * Generic schema for parsing any SupplierPack status change event + */ +export const $SupplierPackEvent = EventEnvelope( + "supplier-pack", + "supplier-pack", + $SupplierPack, + packStatuses, +).meta({ + title: "supplier-pack.* Event", + description: "Generic event schema for supplier pack changes", +}); + +/** + * Specialise the generic event schema for a single status + * @param status + */ +function specialiseSupplierPackEvent(status: (typeof packStatuses)[number]) { + const lcStatus = status.toLowerCase(); + return EventEnvelope( + `supplier-pack.${lcStatus}`, + "supplier-pack", + $SupplierPack + .extend({ + status: z.literal(status), + }) + .meta({ + title: "SupplierPack", + description: `Indicates that a specific supplier is capable of producing a specific pack specification. + +For this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `supplier-pack.${lcStatus} Event`, + description: `Event schema for supplier pack change to ${status}`, + }); +} + +export const supplierPackEvents = { + "supplier-pack.draft": specialiseSupplierPackEvent("DRAFT"), + "supplier-pack.int": specialiseSupplierPackEvent("INT"), + "supplier-pack.prod": specialiseSupplierPackEvent("PROD"), + "supplier-pack.disabled": specialiseSupplierPackEvent("DISABLED"), +} as const; diff --git a/packages/events/src/events/volume-group-events.ts b/packages/events/src/events/volume-group-events.ts new file mode 100644 index 0000000..258585b --- /dev/null +++ b/packages/events/src/events/volume-group-events.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { $VolumeGroup, VolumeGroup } from "../domain"; +import { EventEnvelope } from "./event-envelope"; + +const statuses = [ + "DRAFT", + "INT", + "PROD", + "DISABLED", +] as const satisfies readonly VolumeGroup["status"][]; + +/** + * Generic schema for parsing any VolumeGroup status change event + */ +export const $VolumeGroupEvent = EventEnvelope( + "volume-group", + "volume-group", + $VolumeGroup, + statuses, +).meta({ + title: "volume-group.* Event", + description: "Generic event schema for volume group changes", +}); + +/** + * Specialise the generic event schema for a single status + * @param status + */ +function specialiseVolumeGroupEvent(status: (typeof statuses)[number]) { + const lcStatus = status.toLowerCase(); + return EventEnvelope( + `volume-group.${lcStatus}`, + "volume-group", + $VolumeGroup + .extend({ + status: z.literal(status), + }) + .meta({ + title: "VolumeGroup", + description: `A volume group representing several lots within a competition framework under which suppliers will be allocated capacity. + +For this event the status is always \`${status}\``, + }), + [status], + ).meta({ + title: `volume-group.${lcStatus} Event`, + description: `Event schema for volume group change to ${status}`, + }); +} + +export const volumeGroupEvents = { + "volume-group.draft": specialiseVolumeGroupEvent("DRAFT"), + "volume-group.int": specialiseVolumeGroupEvent("INT"), + "volume-group.prod": specialiseVolumeGroupEvent("PROD"), + "volume-group.disabled": specialiseVolumeGroupEvent("DISABLED"), +} as const; diff --git a/packages/events/src/examples/specification-examples.ts b/packages/events/src/examples/specification-examples.ts new file mode 100644 index 0000000..028cae8 --- /dev/null +++ b/packages/events/src/examples/specification-examples.ts @@ -0,0 +1,218 @@ +import { + LetterVariant, + LetterVariantId, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/letter-variant"; +import { + EnvelopeId, + PackSpecification, + PackSpecificationId, + PostageId, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { VolumeGroupId } from "../domain"; + +const bauStandardC5: PackSpecification = { + id: PackSpecificationId("bau-standard-c5"), + name: "BAU Standard Letter C5", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 5, + }, + postage: { + id: PostageId("ECONOMY"), + size: "STANDARD", + deliveryDays: 3, + }, + assembly: { + envelopeId: EnvelopeId("envelope-nhs-c5-economy"), + printColour: "BLACK", + }, +}; + +const bauStandardC4: PackSpecification = { + id: PackSpecificationId("bau-standard-c4"), + name: "BAU Standard Letter C4", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 20, + }, + postage: { + id: PostageId("ECONOMY"), + size: "LARGE", + deliveryDays: 3, + }, + assembly: { + envelopeId: EnvelopeId("envelope-nhs-c4-economy"), + printColour: "BLACK", + }, +}; + +const braille: PackSpecification = { + id: PackSpecificationId("braille"), + name: "Braille Letter", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 5, + }, + postage: { + id: PostageId("ARTICLES_BLIND"), + size: "STANDARD", + }, + assembly: { + features: ["BRAILLE"], + printColour: "BLACK", + }, +}; + +const audio: PackSpecification = { + id: PackSpecificationId("audio"), + name: "Audio Letter", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 5, + }, + postage: { + id: PostageId("ARTICLES_BLIND"), + size: "STANDARD", + }, + assembly: { + features: ["AUDIO"], + printColour: "BLACK", + }, +}; + +const sameDay: PackSpecification = { + id: PackSpecificationId("same-day"), + name: "Same Day Letter", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 5, + }, + postage: { + id: PostageId("FIRST"), + size: "LARGE", + deliveryDays: 1, + }, + assembly: { + envelopeId: EnvelopeId("envelope-nhs-c4-same-day"), + printColour: "COLOUR", + }, +}; + +const clientPack1: PackSpecification = { + id: PackSpecificationId("client1-campaign1"), + name: "Client1 Letter Pack 1", + status: "PROD", + version: 1, + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + constraints: { + maxSheets: 4, + }, + postage: { + id: PostageId("ADMAIL"), + size: "STANDARD", + deliveryDays: 3, + }, + assembly: { + envelopeId: EnvelopeId("client1-envelope1-c5"), + features: ["ADMAIL"], + printColour: "COLOUR", + }, +}; + +const packs = { + bauStandardC5, + bauStandardC4, + braille, + audio, + sameDay, + clientPack1, +}; + +const variants: Record = { + bauStandard: { + id: LetterVariantId("bau-standard"), + name: "BAU Standard Letter", + description: "BAU Standard Letter", + volumeGroupId: VolumeGroupId("volume-group-12345"), + packSpecificationIds: [bauStandardC5.id, bauStandardC4.id], + type: "STANDARD", + status: "PROD", + constraints: { + maxSheets: 20, + deliveryDays: 3, + }, + }, + braille: { + id: LetterVariantId("braille"), + name: "Braille Letter", + description: "Braille Letter", + volumeGroupId: VolumeGroupId("volume-group-12345"), + packSpecificationIds: [braille.id], + type: "BRAILLE", + status: "PROD", + constraints: { + maxSheets: 5, + deliveryDays: 3, + }, + }, + audio: { + id: LetterVariantId("audio"), + name: "Audio Letter", + description: "Audio Letter", + volumeGroupId: VolumeGroupId("volume-group-12345"), + packSpecificationIds: [audio.id], + type: "AUDIO", + status: "PROD", + constraints: { + maxSheets: 5, + deliveryDays: 3, + }, + }, + sameDay: { + id: LetterVariantId("same-day"), + name: "Same Day Letter", + description: "Same Day Letter", + volumeGroupId: VolumeGroupId("volume-group-12345"), + packSpecificationIds: [sameDay.id], + type: "STANDARD", + status: "PROD", + constraints: { + maxSheets: 5, + deliveryDays: 1, + }, + }, + campaign1: { + id: LetterVariantId("client1"), + name: "Client 1 Letter Variant 1", + description: "Client 1 Letter Variant 1", + volumeGroupId: VolumeGroupId("volume-group-campaign1"), + packSpecificationIds: [clientPack1.id], + type: "STANDARD", + status: "PROD", + clientId: "client1", + campaignIds: ["client1-campaign1"], + constraints: { + maxSheets: 4, + deliveryDays: 3, + }, + }, +}; + +// eslint-disable-next-line no-console +console.log(JSON.stringify({ packs, variants }, null, 2)); diff --git a/packages/events/src/helpers/__tests__/id-ref.test.ts b/packages/events/src/helpers/__tests__/id-ref.test.ts new file mode 100644 index 0000000..1c1ea1a --- /dev/null +++ b/packages/events/src/helpers/__tests__/id-ref.test.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import { idRef } from "../id-ref"; + +describe("idRef", () => { + describe("when the id field is present", () => { + it("should create a reference using the default 'id' field", () => { + const CustomerSchema = z.object({ + id: z.string(), + name: z.string(), + }); + + const refSchema = idRef(CustomerSchema); + + expect(refSchema.parse("customer-123")).toBe("customer-123"); + }); + + it("should create a reference using a custom id field", () => { + const CustomerSchema = z.object({ + customerId: z.string(), + name: z.string(), + }); + + const refSchema = idRef(CustomerSchema, "customerId"); + + expect(refSchema.parse("customer-123")).toBe("customer-123"); + }); + + it("should add metadata to the reference schema", () => { + const CustomerSchema = z + .object({ + id: z.string(), + name: z.string(), + }) + .describe("Customer"); + + const refSchema = idRef(CustomerSchema); + + expect(refSchema.description).toBe( + "Reference to a Customer by its unique identifier", + ); + }); + + it("should use provided entity name in metadata", () => { + const Schema = z.object({ + id: z.string(), + name: z.string(), + }); + + const refSchema = idRef(Schema, undefined, "CustomEntity"); + + expect(refSchema.description).toBe( + "Reference to a CustomEntity by its unique identifier", + ); + }); + + it("should infer the correct type from the referenced schema", () => { + const CustomerSchema = z.object({ + id: z.number(), + name: z.string(), + }); + + const refSchema = idRef(CustomerSchema); + + expect(refSchema.parse(123)).toBe(123); + expect(() => refSchema.parse("not-a-number")).toThrow(); + }); + }); + + describe("when the id field is not present", () => { + it("should throw an error when default 'id' field is missing", () => { + const CustomerSchema = z.object({ + customerId: z.string(), + name: z.string(), + }) as any; // TypeScript won't allow this, but we're testing runtime behavior + + expect(() => idRef(CustomerSchema)).toThrow( + "ID field 'id' not found in schema", + ); + }); + + it("should throw an error when custom id field is missing", () => { + const CustomerSchema = z.object({ + id: z.string(), + name: z.string(), + }); + + expect(() => idRef(CustomerSchema, "nonExistentId" as any)).toThrow( + "ID field 'nonExistentId' not found in schema", + ); + }); + }); +}); diff --git a/packages/schemas/src/helpers/id-ref.ts b/packages/events/src/helpers/id-ref.ts similarity index 57% rename from packages/schemas/src/helpers/id-ref.ts rename to packages/events/src/helpers/id-ref.ts index 19d81aa..90b2573 100644 --- a/packages/schemas/src/helpers/id-ref.ts +++ b/packages/events/src/helpers/id-ref.ts @@ -1,6 +1,9 @@ import { z } from "zod"; +import { getEntityName } from "zod-mermaid"; /** + * Vendored from zod-mermaid to avoid a production dependency on that package. + * * Creates a field that references another entity by ID, inferring the type from the referenced * schema's id field. * This allows you to indicate relationships without embedding the full entity. @@ -21,31 +24,44 @@ import { z } from "zod"; * customerId: idRef(CustomerSchema), // Inferred as ZodString * }); */ +// Overload for when a specific ID field is provided +export function idRef< + T extends z.ZodObject>, + K extends keyof T["shape"] & string, +>(schema: T, idFieldName: K, entityName?: string): T["shape"][K]; + +// Overload for when using the default "id" field +export function idRef< + T extends z.ZodObject> & { + shape: { id: z.ZodTypeAny }; + }, +>(schema: T, idFieldName?: undefined, entityName?: string): T["shape"]["id"]; + +// Implementation export function idRef< T extends z.ZodObject>, - K extends keyof z.infer & string = "id", + K extends keyof T["shape"] & string = "id", >(schema: T, idFieldName?: K, entityName?: string): T["shape"][K] { const { shape } = schema; const field = idFieldName ?? "id"; - if (!(field in shape)) { - throw new Error(`ID field '${field}' not found in schema`); - } - // Get the ID field schema + // eslint-disable-next-line security/detect-object-injection const idFieldSchema = shape[field]; if (!idFieldSchema) { throw new Error(`ID field '${field}' not found in schema`); } // Use the provided entity name or the schema description - const targetEntityName = entityName || schema.description || "Unknown"; + const targetEntityName = + entityName || getEntityName(schema, z.globalRegistry) || "Entity"; // Create a new schema with the same type and validation as the ID field - const resultSchema = idFieldSchema.clone(); - - // Add metadata to indicate this is an ID reference - (resultSchema as any).__idRef = targetEntityName; + const resultSchema = idFieldSchema.clone().meta({ + title: `${targetEntityName} ID Reference`, + description: `Reference to a ${targetEntityName} by its unique identifier`, + targetEntityName, + }); return resultSchema as T["shape"][K]; } diff --git a/packages/events/src/index.ts b/packages/events/src/index.ts new file mode 100644 index 0000000..ec766a7 --- /dev/null +++ b/packages/events/src/index.ts @@ -0,0 +1,16 @@ +export * from "./domain/channel"; +export * from "./domain/common"; +export * from "./domain/volume-group"; +export * from "./domain/letter-variant"; +export * from "./domain/pack-specification"; +export * from "./domain/supplier"; +export * from "./domain/supplier-allocation"; +export * from "./domain/supplier-pack"; +export * from "./events/volume-group-events"; +export * from "./events/event-envelope"; +export * from "./events/pack-specification-events"; +export * from "./events/supplier-allocation-events"; +export * from "./events/supplier-pack-events"; +export * from "./events/supplier-events"; +export {$Postage} from "./domain"; +export {$Envelope} from "./domain"; diff --git a/packages/events/tsconfig.build.json b/packages/events/tsconfig.build.json new file mode 100644 index 0000000..c274012 --- /dev/null +++ b/packages/events/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "exclude": [ + "node_modules", + "dist", + "src/cli", + "src/examples", + "src/**/__tests__" + ], + "extends": "./tsconfig.json", + "include": [ + "src/**/*" + ] +} diff --git a/packages/events/tsconfig.jest.json b/packages/events/tsconfig.jest.json new file mode 100644 index 0000000..b612677 --- /dev/null +++ b/packages/events/tsconfig.jest.json @@ -0,0 +1,9 @@ +{ + "exclude": [ + "./dist/" + ], + "extends": "./tsconfig.json", + "include": [ + "." + ] +} diff --git a/packages/events/tsconfig.json b/packages/events/tsconfig.json new file mode 100644 index 0000000..3426c90 --- /dev/null +++ b/packages/events/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "isolatedModules": true, + "module": "commonjs", + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src" + }, + "exclude": [ + "node_modules", + "dist" + ], + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*", + "jest.config.ts", + "package.json" + ] +} diff --git a/packages/excel-parser/README.md b/packages/excel-parser/README.md new file mode 100644 index 0000000..d49af3e --- /dev/null +++ b/packages/excel-parser/README.md @@ -0,0 +1,124 @@ +# Excel Parser + +A package for parsing and generating Excel files for NHS Notify supplier configuration data. + +## Features + +- Parse Excel files containing supplier configuration data (PackSpecification, LetterVariant, VolumeGroup, Supplier, SupplierAllocation, SupplierPack) +- Generate template Excel files with the correct sheet structure and headers +- Validate parsed data against Zod schemas + +## Usage + +### CLI Commands + +#### Parse Excel to JSON + +Parse an Excel file and output the parsed JSON data: + +```bash +# Parse and output to stdout +npm run parse -- config.xlsx + +# Parse and pretty-print to stdout +npm run parse -- config.xlsx --pretty + +# Parse and save to file +npm run parse -- config.xlsx -o output.json + +# Parse with pretty formatting and save to file +npm run parse -- config.xlsx --pretty --output output.json + +# Show help +npm run parse -- --help +``` + +**Options:** + +- `-o, --output ` - Write output to a file instead of stdout +- `-p, --pretty` - Pretty-print the JSON output +- `-h, --help` - Show help message + +**Output format:** + +The JSON output contains the following top-level keys: + +- `packs` - Record of PackSpecification objects keyed by ID +- `variants` - Record of LetterVariant objects keyed by ID +- `volumeGroups` - Record of VolumeGroup objects keyed by ID +- `suppliers` - Record of Supplier objects keyed by ID +- `allocations` - Record of SupplierAllocation objects keyed by ID +- `supplierPacks` - Record of SupplierPack objects keyed by ID + +#### Generate Template + +Generate a template Excel file with the correct sheet structure and column headers: + +```bash +# Generate a new template +npm run template -- output.xlsx + +# Overwrite existing template +npm run template -- output.xlsx --force + +# Show help +npm run template -- --help +``` + +**Options:** + +- `-f, --force` - Overwrite existing file if it exists +- `-h, --help` - Show help message + +**Generated sheets:** + +The template includes the following sheets with proper column headers: + +- `PackSpecification` - Pack specification definitions +- `LetterVariant` - Letter variant configurations +- `VolumeGroup` - Volume group definitions +- `Supplier` - Supplier information +- `SupplierAllocation` - Supplier allocation percentages +- `SupplierPack` - Supplier-pack mappings + +### Programmatic Usage + +#### Parsing an Excel file + +```typescript +import { parseExcelFile } from "@nhs-notify/excel-parser"; + +const result = parseExcelFile("./specifications.xlsx"); + +console.log(result.packs); // Record +console.log(result.variants); // Record +console.log(result.volumeGroups); // Record +console.log(result.suppliers); // Record +console.log(result.allocations); // Record +console.log(result.supplierPacks); // Record +``` + +### Generating a template Excel file + +```typescript +import { generateTemplateExcel } from "@nhs-notify/excel-parser"; + +// Generate a new template (fails if file exists) +generateTemplateExcel("./specifications.template.xlsx"); + +// Force overwrite existing file +generateTemplateExcel("./specifications.template.xlsx", true); +``` + +## Required Excel Sheets + +The Excel file must contain the following sheets: + +1. **PackSpecification** - Pack specification definitions +2. **LetterVariant** - Letter variant configurations +3. **VolumeGroup** - Volume group definitions +4. **Supplier** - Supplier information +5. **SupplierAllocation** - Supplier allocation percentages per volume group +6. **SupplierPack** - Mapping of suppliers to pack specifications + +See `EXCEL_HEADERS.md` for detailed column specifications. diff --git a/packages/excel-parser/examples/example_specifications.xlsx b/packages/excel-parser/examples/example_specifications.xlsx new file mode 100644 index 0000000..c45f61a Binary files /dev/null and b/packages/excel-parser/examples/example_specifications.xlsx differ diff --git a/packages/excel-parser/jest.config.ts b/packages/excel-parser/jest.config.ts new file mode 100644 index 0000000..1bce48b --- /dev/null +++ b/packages/excel-parser/jest.config.ts @@ -0,0 +1,67 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "./.reports/unit/coverage", + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ["/__tests__/"], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], + + moduleNameMapper: { + "^@supplier-config/excel-parser/(.*)$": "/src/$1", + }, + + testEnvironment: "node", + testPathIgnorePatterns: ["/node_modules/", "/dist/"], + testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + moduleFileExtensions: ["ts", "js", "json", "node"], + + transform: { + "^.+\\.tsx?$": [ + "@swc/jest", + { + jsc: { + parser: { + syntax: "typescript", + tsx: false, + }, + target: "es2022", + }, + }, + ], + }, +}; + +export default config; diff --git a/packages/excel-parser/package.json b/packages/excel-parser/package.json new file mode 100644 index 0000000..3646a88 --- /dev/null +++ b/packages/excel-parser/package.json @@ -0,0 +1,33 @@ +{ + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-config": "*", + "xlsx": "^0.18.5", + "yargs": "^17.7.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "@types/xlsx": "^0.0.35", + "@types/yargs": "^17.0.33", + "jest": "^29.7.0", + "jest-html-reporter": "^3.10.2", + "jest-mock-extended": "^3.0.7", + "typescript": "^5.9.3" + }, + "name": "@supplier-config/excel-parser", + "private": true, + "scripts": { + "build": "tsc", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "parse": "tsx src/cli-parse.ts", + "template": "tsx src/cli-template.ts", + "test": "jest", + "test:unit": "jest", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/packages/excel-parser/src/__tests__/parse-excel.test.ts b/packages/excel-parser/src/__tests__/parse-excel.test.ts new file mode 100644 index 0000000..4c3a4e1 --- /dev/null +++ b/packages/excel-parser/src/__tests__/parse-excel.test.ts @@ -0,0 +1,2116 @@ +import * as XLSX from "xlsx"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { parseExcelFile } from "../parse-excel"; + +function buildWorkbook(data: { + packs: any[]; + variants: any[]; + volumeGroups?: any[]; + suppliers?: any[]; + allocations?: any[]; + supplierPacks?: any[]; +}) { + const wb = XLSX.utils.book_new(); + const packSheet = XLSX.utils.json_to_sheet(data.packs); + const variantSheet = XLSX.utils.json_to_sheet(data.variants); + XLSX.utils.book_append_sheet(wb, packSheet, "PackSpecification"); + XLSX.utils.book_append_sheet(wb, variantSheet, "LetterVariant"); + + // Add required new sheets with defaults if not provided + const volumeGroupSheet = XLSX.utils.json_to_sheet(data.volumeGroups || []); + XLSX.utils.book_append_sheet(wb, volumeGroupSheet, "VolumeGroup"); + + const supplierSheet = XLSX.utils.json_to_sheet(data.suppliers || []); + XLSX.utils.book_append_sheet(wb, supplierSheet, "Supplier"); + + const allocationSheet = XLSX.utils.json_to_sheet(data.allocations || []); + XLSX.utils.book_append_sheet(wb, allocationSheet, "SupplierAllocation"); + + const supplierPackSheet = XLSX.utils.json_to_sheet(data.supplierPacks || []); + XLSX.utils.book_append_sheet(wb, supplierPackSheet, "SupplierPack"); + + return wb; +} + +// Replace individual missing sheet tests with table-driven tests +function buildWorkbookOmitting(omit: string): XLSX.WorkBook { + const wb = XLSX.utils.book_new(); + const packSheet = XLSX.utils.json_to_sheet([ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ]); + if (omit !== "PackSpecification") { + XLSX.utils.book_append_sheet(wb, packSheet, "PackSpecification"); + } + if (omit !== "LetterVariant") { + XLSX.utils.book_append_sheet( + wb, + XLSX.utils.json_to_sheet([]), + "LetterVariant", + ); + } + if (omit !== "VolumeGroup") { + XLSX.utils.book_append_sheet( + wb, + XLSX.utils.json_to_sheet([]), + "VolumeGroup", + ); + } + if (omit !== "Supplier") { + XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([]), "Supplier"); + } + if (omit !== "SupplierAllocation") { + XLSX.utils.book_append_sheet( + wb, + XLSX.utils.json_to_sheet([]), + "SupplierAllocation", + ); + } + if (omit !== "SupplierPack") { + XLSX.utils.book_append_sheet( + wb, + XLSX.utils.json_to_sheet([]), + "SupplierPack", + ); + } + return wb; +} + +function writeWorkbook(wb: XLSX.WorkBook): string { + const filePath = path.join(tmpdir(), `test-${Date.now()}.xlsx`); + XLSX.writeFile(wb, filePath); + return filePath; +} + +describe("parse-excel", () => { + it("parses canonical enum values", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-standard", + "postage.size": "STANDARD", + }, + { + id: "pack-2", + name: "Pack 2", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-large", + "postage.size": "LARGE", + }, + ], + variants: [ + { + id: "variant-1", + name: "Variant 1", + description: "Variant 1", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1,pack-2", + type: "STANDARD", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.pack1.postage.id).toBe("postage-standard"); + expect(result.packs.pack1.postage.size).toBe("STANDARD"); + expect(result.packs.pack2.postage.id).toBe("postage-large"); + expect(result.packs.pack2.postage.size).toBe("LARGE"); + expect(result.variants.variant1.packSpecificationIds).toEqual([ + "pack-1", + "pack-2", + ]); + }); + + it("throws on invalid postage size", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-bad-size", + name: "Bad Size Pack", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-bad", + "postage.size": "C5", // invalid size value + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Validation failed.*pack-bad-size/, + ); + }); + + it("throws on missing required postage fields", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-missing", + name: "Missing Pack", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + // missing postage fields entirely + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Missing required postage fields/, + ); + }); + + it("parses constraints on PackSpecification", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-constraints", + name: "Pack with Constraints", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "constraints.sheets": "10", + "constraints.deliveryDays": "5", + "constraints.blackCoveragePercentage": "80.5", + "constraints.colourCoveragePercentage": "50.25", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithconstraints.constraints).toEqual({ + sheets: { value: 10, operator: "LESS_THAN" }, + deliveryDays: { value: 5, operator: "LESS_THAN" }, + blackCoveragePercentage: { value: 80.5, operator: "LESS_THAN" }, + colourCoveragePercentage: { value: 50.25, operator: "LESS_THAN" }, + }); + }); + + it("parses PackSpecification with optional description", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-description", + name: "Pack with Description", + description: "A standard economy-class letter for bulk mailings", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithdescription.description).toBe( + "A standard economy-class letter for bulk mailings", + ); + }); + + it("parses PackSpecification without description", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-without-description", + name: "Pack without Description", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithoutdescription.description).toBeUndefined(); + }); + + it("parses constraints on LetterVariant", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-with-constraints", + name: "Variant with Constraints", + description: "Test variant", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1", + type: "STANDARD", + status: "PROD", + "constraints.sheets": "8", + "constraints.deliveryDays": "3", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantwithconstraints.constraints).toEqual({ + sheets: { value: 8, operator: "LESS_THAN" }, + deliveryDays: { value: 3, operator: "LESS_THAN" }, + }); + }); + + it("parses assembly with paper, insertIds, and features", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-assembly", + name: "Pack with Assembly", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "envelope-1", + "assembly.printColour": "COLOUR", + "assembly.paper.id": "paper-1", + "assembly.paper.name": "Standard White", + "assembly.paper.weightGSM": "90", + "assembly.paper.size": "A4", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "true", + "assembly.insertIds": "insert-1,insert-2", + "assembly.features": "BRAILLE,AUDIO", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + const pack = result.packs.packwithassembly; + expect(pack.assembly?.envelopeId).toBe("envelope-1"); + expect(pack.assembly?.printColour).toBe("COLOUR"); + expect(pack.assembly?.paper?.id).toBe("paper-1"); + expect(pack.assembly?.paper?.name).toBe("Standard White"); + expect(pack.assembly?.paper?.weightGSM).toBe(90); + expect(pack.assembly?.paper?.size).toBe("A4"); + expect(pack.assembly?.paper?.colour).toBe("WHITE"); + expect(pack.assembly?.paper?.recycled).toBe(true); + expect(pack.assembly?.insertIds).toEqual(["insert-1", "insert-2"]); + expect(pack.assembly?.features).toEqual(["BRAILLE", "AUDIO"]); + }); + + it("parses assembly with duplex set to true", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-duplex-true", + name: "Pack with Duplex True", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "envelope-1", + "assembly.duplex": "true", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithduplextrue.assembly?.duplex).toBe(true); + }); + + it("parses assembly with duplex set to false", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-duplex-false", + name: "Pack with Duplex False", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "envelope-1", + "assembly.duplex": "false", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithduplexfalse.assembly?.duplex).toBe(false); + }); + + it("parses assembly without duplex field", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-without-duplex", + name: "Pack without Duplex", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "envelope-1", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithoutduplex.assembly?.duplex).toBeUndefined(); + }); + + it("thows when LetterVariant sheet is missing", () => { + const wb = XLSX.utils.book_new(); + const packSheet = XLSX.utils.json_to_sheet([ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-standard", + "postage.size": "STANDARD", + }, + ]); + XLSX.utils.book_append_sheet(wb, packSheet, "PackSpecification"); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /LetterVariant sheet not found in Excel file/, + ); + }); + + it("thows when PackSpecification sheet is missing", () => { + const wb = XLSX.utils.book_new(); + const variantSheet = XLSX.utils.json_to_sheet([ + { + id: "variant-1", + name: "Variant 1", + description: "Variant 1", + packSpecificationIds: "pack-1,pack-2", + type: "STANDARD", + status: "PROD", + }, + ]); + XLSX.utils.book_append_sheet(wb, variantSheet, "LetterVariant"); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /PackSpecification sheet not found in Excel file/, + ); + }); + + it("throws if LetterVariant type is invalid", () => { + const wb = XLSX.utils.book_new(); + const variantSheet = XLSX.utils.json_to_sheet([ + { + id: "variant-1", + name: "Variant 1", + description: "Variant 1", + packSpecificationIds: "pack-1", + type: "INVALID_TYPE", + status: "PROD", + }, + ]); + XLSX.utils.book_append_sheet(wb, variantSheet, "LetterVariant"); + const packSheet = XLSX.utils.json_to_sheet([ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-standard", + "postage.size": "STANDARD", + }, + ]); + XLSX.utils.book_append_sheet(wb, packSheet, "PackSpecification"); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow(/Validation failed.*variant-1/); + }); + + it("parses optional billingId field", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-billing", + name: "Pack with Billing", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + billingId: "billing-123", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithbilling.billingId).toBe("billing-123"); + }); + + it("parses optional postage fields", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-full-postage", + name: "Pack with Full Postage", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "LARGE", + "postage.deliveryDays": "2", + "postage.maxWeightGrams": "100.5", + "postage.maxThicknessMm": "5.2", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + const { postage } = result.packs.packfullpostage; + expect(postage.deliveryDays).toBe(2); + expect(postage.maxWeightGrams).toBe(100.5); + expect(postage.maxThicknessMm).toBe(5.2); + }); + + it("handles missing createdAt/updatedAt with default dates", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-no-dates", + name: "Pack No Dates", + status: "PROD", + version: "1", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packnodates.createdAt).toBe("2023-01-01T00:00:00Z"); + expect(result.packs.packnodates.updatedAt).toBe("2023-01-01T00:00:00Z"); + }); + + it("handles invalid date strings with default date", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-invalid-dates", + name: "Pack Invalid Dates", + status: "PROD", + version: "1", + createdAt: "not-a-date", + updatedAt: "also-not-a-date", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packinvaliddates.createdAt).toBe( + "2023-01-01T00:00:00Z", + ); + expect(result.packs.packinvaliddates.updatedAt).toBe( + "2023-01-01T00:00:00Z", + ); + }); + + it("handles empty string arrays as undefined", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-empty-arrays", + name: "Pack Empty Arrays", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.insertIds": "", + "assembly.features": " ", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packemptyarrays.assembly?.insertIds).toBeUndefined(); + expect(result.packs.packemptyarrays.assembly?.features).toBeUndefined(); + }); + + it("parses assembly.additional as JSON", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-additional", + name: "Pack with Additional", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.additional": '{"key1":"value1","key2":"value2"}', + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithadditional.assembly?.additional).toEqual({ + key1: "value1", + key2: "value2", + }); + }); + + it("ignores invalid JSON in assembly.additional", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-bad-json", + name: "Pack Bad JSON", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.additional": "not-valid-json{", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packbadjson.assembly?.additional).toBeUndefined(); + }); + + it("parses paper.recycled as boolean", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-recycled-true", + name: "Pack Recycled True", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.paper.id": "paper-1", + "assembly.paper.name": "Recycled Paper", + "assembly.paper.size": "A4", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "TRUE", + }, + { + id: "pack-recycled-false", + name: "Pack Recycled False", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-2", + "postage.size": "LARGE", + "assembly.paper.id": "paper-2", + "assembly.paper.name": "Non-Recycled Paper", + "assembly.paper.size": "A3", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "false", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packrecycledtrue.assembly?.paper?.recycled).toBe(true); + expect(result.packs.packrecycledfalse.assembly?.paper?.recycled).toBe( + false, + ); + }); + + it("uses default weightGSM when not provided", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-default-gsm", + name: "Pack Default GSM", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.paper.id": "paper-1", + "assembly.paper.size": "A4", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "false", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packdefaultgsm.assembly?.paper?.weightGSM).toBe(80); + }); + + it("parses LetterVariant with optional clientId and campaignIds", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-with-ids", + name: "Variant with IDs", + description: "Test variant", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1", + type: "STANDARD", + status: "PROD", + clientId: "client-123", + campaignIds: "campaign-1,campaign-2,campaign-3", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantwithids.clientId).toBe("client-123"); + expect(result.variants.variantwithids.campaignIds).toEqual([ + "campaign-1", + "campaign-2", + "campaign-3", + ]); + }); + + it("parses LetterVariant with optional supplierId", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-with-supplier", + name: "Variant with Supplier", + description: "Test variant scoped to supplier", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1", + type: "STANDARD", + status: "PROD", + supplierId: "supplier-printco", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantwithsupplier.supplierId).toBe( + "supplier-printco", + ); + }); + + it("uses name as description when description is missing", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-no-desc", + name: "My Variant Name", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1", + type: "STANDARD", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantnodesc.description).toBe("My Variant Name"); + }); + + it("throws on empty packSpecificationIds", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-no-packs", + name: "Variant No Packs", + description: "Test", + packSpecificationIds: "", + type: "STANDARD", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Validation failed.*variant-no-packs/, + ); + }); + + it("sanitizes IDs by removing non-alphanumeric characters", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-dashes-123", + name: "Pack with Dashes", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant_with_underscores_456", + name: "Variant with Underscores", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-with-dashes-123", + type: "STANDARD", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithdashes123).toBeDefined(); + expect(result.variants.variantwithunderscores456).toBeDefined(); + }); + + it("handles partial constraints - only sheets", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-partial-1", + name: "Pack Partial 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "constraints.sheets": "15", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packpartial1.constraints).toEqual({ + sheets: { value: 15, operator: "LESS_THAN" }, + }); + }); + + it("handles partial constraints - only deliveryDays", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-partial-2", + name: "Pack Partial 2", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "constraints.deliveryDays": "7", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packpartial2.constraints).toEqual({ + deliveryDays: { value: 7, operator: "LESS_THAN" }, + }); + }); + + it("handles partial constraints - only coverage percentages", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-partial-3", + name: "Pack Partial 3", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "constraints.blackCoveragePercentage": "90.5", + "constraints.colourCoveragePercentage": "60.25", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packpartial3.constraints).toEqual({ + blackCoveragePercentage: { value: 90.5, operator: "LESS_THAN" }, + colourCoveragePercentage: { value: 60.25, operator: "LESS_THAN" }, + }); + }); + + it("parses assembly with only envelopeId", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-envelope-only", + name: "Pack Envelope Only", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "envelope-123", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packenvelopeonly.assembly).toEqual({ + envelopeId: "envelope-123", + }); + }); + + it("parses assembly with only printColour", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-print-only", + name: "Pack Print Only", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.printColour": "BLACK", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packprintonly.assembly).toEqual({ + printColour: "BLACK", + }); + }); + + it("throws when postage.id is missing but postage.size is present", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-missing-id", + name: "Pack Missing ID", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Missing required postage fields.*pack-missing-id/, + ); + }); + + it("throws when postage.size is missing but postage.id is present", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-missing-size", + name: "Pack Missing Size", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Missing required postage fields.*pack-missing-size/, + ); + }); + + it("handles all assembly fields together", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-full-assembly", + name: "Pack Full Assembly", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.envelopeId": "env-1", + "assembly.printColour": "COLOUR", + "assembly.paper.id": "paper-1", + "assembly.paper.name": "Premium", + "assembly.paper.weightGSM": "100", + "assembly.paper.size": "A3", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "true", + "assembly.insertIds": "insert-a,insert-b", + "assembly.features": "BRAILLE,AUDIO,ADMAIL", + "assembly.additional": '{"note":"test"}', + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + const { assembly } = result.packs.packfullassembly; + expect(assembly?.envelopeId).toBe("env-1"); + expect(assembly?.printColour).toBe("COLOUR"); + expect(assembly?.paper?.id).toBe("paper-1"); + expect(assembly?.insertIds).toEqual(["insert-a", "insert-b"]); + expect(assembly?.features).toEqual(["BRAILLE", "AUDIO", "ADMAIL"]); + expect(assembly?.additional).toEqual({ note: "test" }); + }); + + it("parses arrays with whitespace correctly", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-whitespace-arrays", + name: "Pack Whitespace Arrays", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.insertIds": " insert-1 , insert-2 , insert-3 ", + "assembly.features": " BRAILLE , AUDIO ", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwhitespacearrays.assembly?.insertIds).toEqual([ + "insert-1", + "insert-2", + "insert-3", + ]); + expect(result.packs.packwhitespacearrays.assembly?.features).toEqual([ + "BRAILLE", + "AUDIO", + ]); + }); + + it("handles empty insertIds but valid features", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-empty-inserts", + name: "Pack Empty Inserts", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.insertIds": " ", + "assembly.features": "BRAILLE", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packemptyinserts.assembly?.insertIds).toBeUndefined(); + expect(result.packs.packemptyinserts.assembly?.features).toEqual([ + "BRAILLE", + ]); + }); + + it("handles valid insertIds but empty features", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-empty-features", + name: "Pack Empty Features", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.insertIds": "insert-1", + "assembly.features": " ", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packemptyfeatures.assembly?.insertIds).toEqual([ + "insert-1", + ]); + expect(result.packs.packemptyfeatures.assembly?.features).toBeUndefined(); + }); + + const missingSheetCases: { sheet: string; error: RegExp }[] = [ + { sheet: "VolumeGroup", error: /VolumeGroup sheet not found/ }, + { sheet: "Supplier", error: /Supplier sheet not found/ }, + { + sheet: "SupplierAllocation", + error: /SupplierAllocation sheet not found/, + }, + { sheet: "SupplierPack", error: /SupplierPack sheet not found/ }, + ]; + + for (const { error, sheet } of missingSheetCases) { + it(`throws when ${sheet} sheet is missing (table-driven)`, () => { + const wb = buildWorkbookOmitting(sheet); + // Ensure we are only consolidating the four; PackSpecification & LetterVariant already tested separately. + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow(error); + }); + } + + it("parses VolumeGroup with description and endDate", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-x", + name: "Pack X", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-02", + "postage.id": "postage-x", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-x", + name: "Variant X", + volumeGroupId: "volume-group-x", + packSpecificationIds: "pack-x", + type: "STANDARD", + status: "PROD", + }, + ], + volumeGroups: [ + { + id: "volume-group-x", + name: "VolumeGroup X", + description: "My VolumeGroup", + startDate: "2025-01-01", + endDate: "2025-12-31", + status: "PROD", + }, + ], + suppliers: [], + allocations: [], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.volumeGroups.volumegroupx.description).toBe("My VolumeGroup"); + expect(result.volumeGroups.volumegroupx.endDate).toBe("2025-12-31"); + expect(result.volumeGroups.volumegroupx.startDate).toBe("2025-01-01"); + expect(result.volumeGroups.volumegroupx.status).toBe("PROD"); + }); + + it("parses Suppliers of all channel types", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-y", + name: "Pack Y", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-y", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [], + suppliers: [ + { + id: "supplier-app", + name: "App Supplier", + channelType: "NHSAPP", + dailyCapacity: "1000", + status: "PROD", + }, + { + id: "supplier-sms", + name: "SMS Supplier", + channelType: "SMS", + dailyCapacity: "2000", + status: "PROD", + }, + { + id: "supplier-email", + name: "Email Supplier", + channelType: "EMAIL", + dailyCapacity: "3000", + status: "PROD", + }, + { + id: "supplier-letter", + name: "Letter Supplier", + channelType: "LETTER", + dailyCapacity: "4000", + status: "PROD", + }, + ], + allocations: [], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.suppliers.supplierapp.channelType).toBe("NHSAPP"); + expect(result.suppliers.suppliersms.channelType).toBe("SMS"); + expect(result.suppliers.supplieremail.channelType).toBe("EMAIL"); + expect(result.suppliers.supplierletter.channelType).toBe("LETTER"); + }); + + it("throws on invalid Supplier channelType", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-z", + name: "Pack Z", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-z", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [], + suppliers: [ + { + id: "supplier-bad", + name: "Bad Supplier", + channelType: "PIGEON", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Validation failed.*supplier-bad/, + ); + }); + + it("parses SupplierAllocations including boundary percentages and sanitizes IDs", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-a", + name: "Pack A", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-a", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group-a", + name: "VolumeGroup A", + startDate: "2025-01-01", + status: "PROD", + }, + ], + suppliers: [ + { + id: "supplier-a", + name: "Supplier A", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + { + id: "supplier-b", + name: "Supplier B", + channelType: "LETTER", + dailyCapacity: "2000", + status: "PROD", + }, + { + id: "supplier-c", + name: "Supplier C", + channelType: "LETTER", + dailyCapacity: "3000", + status: "PROD", + }, + ], + allocations: [ + { + id: "allocation-1%", // sanitized + volumeGroupId: "volume-group-a", + supplier: "supplier-a", + allocationPercentage: "0", + status: "PROD", + }, + { + id: "allocation-2%", + volumeGroupId: "volume-group-a", + supplier: "supplier-b", + allocationPercentage: "75", + status: "PROD", + }, + { + id: "allocation-3%", + volumeGroupId: "volume-group-a", + supplier: "supplier-c", + allocationPercentage: "100", + status: "PROD", + }, + ], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.allocations.allocation1.volumeGroup).toBe("volume-group-a"); + expect(result.allocations.allocation2.allocationPercentage).toBe(75); + expect(result.allocations.allocation3.allocationPercentage).toBe(100); + }); + + it("throws when allocationPercentage is out of bounds (<0 or >100)", () => { + const wbHigh = buildWorkbook({ + packs: [ + { + id: "pack-bounds", + name: "Pack Bounds", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-bounds", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group-bounds", + name: "VolumeGroup Bounds", + startDate: "2025-01-01", + status: "PROD", + }, + ], + suppliers: [ + { + id: "supplier-bounds", + name: "Supplier Bounds", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [ + { + id: "allocation-high", + volumeGroupId: "volume-group-bounds", + supplier: "supplier-bounds", + allocationPercentage: "150", + status: "PROD", + }, + ], + supplierPacks: [], + }); + const fileHigh = writeWorkbook(wbHigh); + expect(() => parseExcelFile(fileHigh)).toThrow( + /Validation failed.*allocation-high/, + ); + + const wbLow = buildWorkbook({ + packs: [ + { + id: "pack-bounds2", + name: "Pack Bounds 2", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-bounds2", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group-bounds2", + name: "VolumeGroup Bounds 2", + startDate: "2025-01-01", + status: "PROD", + }, + ], + suppliers: [ + { + id: "supplier-bounds2", + name: "Supplier Bounds 2", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [ + { + id: "allocation-low", + volumeGroupId: "volume-group-bounds2", + supplier: "supplier-bounds2", + allocationPercentage: "-5", + status: "PROD", + }, + ], + supplierPacks: [], + }); + const fileLow = writeWorkbook(wbLow); + expect(() => parseExcelFile(fileLow)).toThrow( + /Validation failed.*allocation-low/, + ); + }); + + it("parses SupplierPack rows for all statuses", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-sp", + name: "Pack SP", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-sp", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [], + suppliers: [ + { + id: "supplier-sp", + name: "Supplier SP", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [], + supplierPacks: [ + { + id: "sp-submitted", + packSpecificationId: "pack-sp", + supplierId: "supplier-sp", + approval: "SUBMITTED", + status: "PROD", + }, + { + id: "sp-approved", + packSpecificationId: "pack-sp", + supplierId: "supplier-sp", + approval: "APPROVED", + status: "PROD", + }, + { + id: "sp-rejected", + packSpecificationId: "pack-sp", + supplierId: "supplier-sp", + approval: "REJECTED", + status: "PROD", + }, + { + id: "sp-disabled", + packSpecificationId: "pack-sp", + supplierId: "supplier-sp", + approval: "DISABLED", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.supplierPacks.spsubmitted.approval).toBe("SUBMITTED"); + expect(result.supplierPacks.spapproved.approval).toBe("APPROVED"); + expect(result.supplierPacks.sprejected.approval).toBe("REJECTED"); + expect(result.supplierPacks.spdisabled.approval).toBe("DISABLED"); + }); + + it("throws on invalid SupplierPack status", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-sp-bad", + name: "Pack SP Bad", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-sp-bad", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [], + suppliers: [ + { + id: "supplier-sp-bad", + name: "Supplier SP Bad", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [], + supplierPacks: [ + { + id: "sp-invalid", + packSpecificationId: "pack-sp-bad", + supplierId: "supplier-sp-bad", + approval: "UNKNOWNSTATUS", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow(/Validation failed.*sp-invalid/); + }); + + it("leaves constraints undefined when none provided", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-no-constraints", + name: "Pack No Constraints", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-nc", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-no-constraints", + name: "Variant No Constraints", + volumeGroupId: "volume-group-nc", + packSpecificationIds: "pack-no-constraints", + type: "STANDARD", + status: "PROD", + }, + ], + volumeGroups: [ + { + id: "volume-group-nc", + name: "VolumeGroup NC", + startDate: "2025-01-01", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packnoconstraints.constraints).toBeUndefined(); + expect(result.variants.variantnoconstraints.constraints).toBeUndefined(); + }); + + it("trims spaces in campaignIds and packSpecificationIds", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-space-1", + name: "Pack Space 1", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-space-1", + "postage.size": "STANDARD", + }, + { + id: "pack-space-2", + name: "Pack Space 2", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-space-2", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-space", + name: "Variant Space", + volumeGroupId: "volume-group-space", + packSpecificationIds: " pack-space-1 , pack-space-2 ", + type: "STANDARD", + status: "PROD", + campaignIds: " campaign-1 , campaign-2 ", + }, + ], + volumeGroups: [ + { + id: "volume-group-space", + name: "VolumeGroup Space", + startDate: "2025-01-01", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantspace.packSpecificationIds).toEqual([ + "pack-space-1", + "pack-space-2", + ]); + expect(result.variants.variantspace.campaignIds).toEqual([ + "campaign-1", + "campaign-2", + ]); + }); + + it("parses volume group and supplier IDs with special chars sanitizing keys", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-sanitize", + name: "Pack Sanitize", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-sanitize", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group#sanitize", + name: "VolumeGroup Sanitize", + startDate: "2025-01-01", + status: "PROD", + }, + ], + suppliers: [ + { + id: "supplier@sanitize", + name: "Supplier Sanitize", + channelType: "LETTER", + dailyCapacity: "1000", + status: "PROD", + }, + ], + allocations: [], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.volumeGroups.volumegroupsanitize.name).toBe( + "VolumeGroup Sanitize", + ); + expect(result.suppliers.suppliersanitize.name).toBe("Supplier Sanitize"); + }); + + it("throws validation error for VolumeGroup with missing name", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-volume-group-bad", + name: "Pack VolumeGroup Bad", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-volume-group-bad", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-volume-group-bad", + name: "Variant VolumeGroup Bad", + volumeGroupId: "volume-group-bad", + packSpecificationIds: "pack-volume-group-bad", + type: "STANDARD", + status: "PROD", + }, + ], + // VolumeGroup row intentionally missing 'name' field to trigger validation error + volumeGroups: [ + { + id: "volume-group-bad", + startDate: "2025-01-01", + status: "PROD", + }, + ], + suppliers: [], + allocations: [], + supplierPacks: [], + }); + const file = writeWorkbook(wb); + expect(() => parseExcelFile(file)).toThrow( + /Validation failed.*volume-group-bad/, + ); + }); + + it("defaults volume group startDate when missing or invalid", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-default-date", + name: "Pack Default Date", + status: "PROD", + version: "1", + createdAt: "2025-01-01", + updatedAt: "2025-01-01", + "postage.id": "postage-default-date", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-default-date", + name: "Variant Default Date", + volumeGroupId: "volume-group-missing-date", + packSpecificationIds: "pack-default-date", + type: "STANDARD", + status: "PROD", + }, + { + id: "variant-invalid-date", + name: "Variant Invalid Date", + volumeGroupId: "volume-group-invalid-date", + packSpecificationIds: "pack-default-date", + type: "STANDARD", + status: "PROD", + }, + ], + volumeGroups: [ + // Missing startDate entirely (will default) + { + id: "volume-group-missing-date", + name: "VolumeGroup Missing Date", + // no startDate provided + status: "PROD", + }, + // Invalid startDate string (will default) + { + id: "volume-group-invalid-date", + name: "VolumeGroup Invalid Date", + startDate: "not-a-valid-date", + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.volumeGroups.volumegroupmissingdate.startDate).toBe( + "2023-01-01", + ); + expect(result.volumeGroups.volumegroupinvaliddate.startDate).toBe( + "2023-01-01", + ); + }); + + it("parses Excel serial date numbers for timestamps", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-excel-dates", + name: "Pack with Excel Serial Dates", + status: "PROD", + version: "1", + createdAt: 44927, // Excel serial date for 2023-01-01 + updatedAt: 44958, // Excel serial date for 2023-02-01 + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packexceldates.createdAt).toMatch(/2023-01-01/); + expect(result.packs.packexceldates.updatedAt).toMatch(/2023-02-01/); + }); + + it("parses Excel serial date numbers for date-only fields", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group-excel-dates", + name: "VolumeGroup with Excel Dates", + startDate: 44927, // Excel serial date for 2023-01-01 + endDate: 45292, // Excel serial date for 2024-01-01 + status: "PROD", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.volumeGroups.volumegroupexceldates.startDate).toBe( + "2023-01-01", + ); + expect(result.volumeGroups.volumegroupexceldates.endDate).toBe( + "2024-01-01", + ); + }); + + it("parses constraints.sides on PackSpecification", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-sides", + name: "Pack with Sides Constraint", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "constraints.sheets": "10", + "constraints.sides": "20", + "constraints.deliveryDays": "5", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithsides.constraints).toEqual({ + sheets: { value: 10, operator: "LESS_THAN" }, + sides: { value: 20, operator: "LESS_THAN" }, + deliveryDays: { value: 5, operator: "LESS_THAN" }, + }); + }); + + it("parses constraints.sides on LetterVariant", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [ + { + id: "variant-with-sides", + name: "Variant with Sides Constraint", + volumeGroupId: "volume-group-1", + packSpecificationIds: "pack-1", + type: "STANDARD", + status: "PROD", + "constraints.sheets": "8", + "constraints.sides": "16", + "constraints.deliveryDays": "3", + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.variants.variantwithsides.constraints).toEqual({ + sheets: { value: 8, operator: "LESS_THAN" }, + sides: { value: 16, operator: "LESS_THAN" }, + deliveryDays: { value: 3, operator: "LESS_THAN" }, + }); + }); + + it("parses assembly.paper.finish", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-with-paper-finish", + name: "Pack with Paper Finish", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.paper.id": "paper-glossy", + "assembly.paper.name": "Glossy Paper", + "assembly.paper.weightGSM": "120", + "assembly.paper.size": "A4", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "true", + "assembly.paper.finish": "GLOSSY", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithpaperfinish.assembly?.paper).toEqual({ + id: "paper-glossy", + name: "Glossy Paper", + weightGSM: 120, + size: "A4", + colour: "WHITE", + recycled: true, + finish: "GLOSSY", + }); + }); + + it("parses assembly.paper without finish", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-without-paper-finish", + name: "Pack without Paper Finish", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.paper.id": "paper-plain", + "assembly.paper.name": "Plain Paper", + "assembly.paper.weightGSM": "80", + "assembly.paper.size": "A4", + "assembly.paper.colour": "WHITE", + "assembly.paper.recycled": "false", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithoutpaperfinish.assembly?.paper).toEqual({ + id: "paper-plain", + name: "Plain Paper", + weightGSM: 80, + size: "A4", + colour: "WHITE", + recycled: false, + }); + expect( + result.packs.packwithoutpaperfinish.assembly?.paper?.finish, + ).toBeUndefined(); + }); + + it("defaults paper.colour to WHITE when missing", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-without-paper-colour", + name: "Pack without Paper Colour", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + "assembly.paper.id": "paper-default-colour", + "assembly.paper.name": "Paper Default Colour", + "assembly.paper.weightGSM": "80", + "assembly.paper.size": "A4", + // no colour field provided + "assembly.paper.recycled": "false", + }, + ], + variants: [], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.packs.packwithoutpapercolour.assembly?.paper?.colour).toBe( + "WHITE", + ); + }); + + it("defaults VolumeGroup status to DRAFT when missing", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + volumeGroups: [ + { + id: "volume-group-no-status", + name: "VolumeGroup without Status", + startDate: "2024-01-01", + // no status field + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.volumeGroups.volumegroupnostatus.status).toBe("DRAFT"); + }); + + it("defaults Supplier status to DRAFT when missing", () => { + const wb = buildWorkbook({ + packs: [ + { + id: "pack-1", + name: "Pack 1", + status: "PROD", + version: "1", + createdAt: "2024-01-01", + updatedAt: "2024-01-01", + "postage.id": "postage-1", + "postage.size": "STANDARD", + }, + ], + variants: [], + suppliers: [ + { + id: "supplier-no-status", + name: "Supplier without Status", + channelType: "LETTER", + dailyCapacity: "5000", + // no status field + }, + ], + }); + const file = writeWorkbook(wb); + const result = parseExcelFile(file); + expect(result.suppliers.suppliernostatus.status).toBe("DRAFT"); + }); +}); diff --git a/packages/excel-parser/src/__tests__/template.test.ts b/packages/excel-parser/src/__tests__/template.test.ts new file mode 100644 index 0000000..8c79eac --- /dev/null +++ b/packages/excel-parser/src/__tests__/template.test.ts @@ -0,0 +1,97 @@ +/* eslint-disable sonarjs/no-alphabetical-sort,security/detect-non-literal-fs-filename */ +import fs from "node:fs"; +import path from "node:path"; +import * as XLSX from "xlsx"; +import generateTemplateExcel from "../template"; + +describe("generateTemplateExcel", () => { + const tmpFile = path.join(process.cwd(), `specs.template.${Date.now()}.xlsx`); + + afterAll(() => { + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + }); + + it("creates an .xlsx with all expected sheets", () => { + const out = generateTemplateExcel(tmpFile, true); + expect(out).toBe(tmpFile); + expect(fs.existsSync(out)).toBe(true); + const wb = XLSX.readFile(out); + const sheetNames = wb.SheetNames.toSorted(); + expect(sheetNames).toEqual( + [ + "VolumeGroup", + "LetterVariant", + "PackSpecification", + "Supplier", + "SupplierAllocation", + "SupplierPack", + ].toSorted(), + ); + }); + + it("populates header row for PackSpecification", () => { + const out = generateTemplateExcel(tmpFile, true); + const wb = XLSX.readFile(out); + const packSheet = wb.Sheets.PackSpecification; + const headers = XLSX.utils.sheet_to_json(packSheet, { + header: 1, + })[0]; + expect(headers).toContain("postage.id"); + expect(headers).toContain("assembly.paper.recycled"); + expect(headers).toContain("constraints.blackCoveragePercentage"); + }); + + it("throws without force when file exists", () => { + generateTemplateExcel(tmpFile, true); + expect(() => generateTemplateExcel(tmpFile, false)).toThrow( + /already exists/, + ); + }); + + it("overwrites file when force is true", () => { + const overwriteFile = path.join( + process.cwd(), + `template.overwrite.${Date.now()}.xlsx`, + ); + // Create initial file + const firstOut = generateTemplateExcel(overwriteFile, false); + expect(fs.existsSync(firstOut)).toBe(true); + const firstStat = fs.statSync(firstOut); + + // Wait a tiny bit to ensure timestamp difference + const start = Date.now(); + // eslint-disable-next-line no-empty + while (Date.now() - start < 10) {} + + // Overwrite with force=true + const secondOut = generateTemplateExcel(overwriteFile, true); + expect(secondOut).toBe(overwriteFile); + expect(fs.existsSync(secondOut)).toBe(true); + const secondStat = fs.statSync(secondOut); + + // File was overwritten (timestamps should be different) + expect(secondStat.mtimeMs).toBeGreaterThanOrEqual(firstStat.mtimeMs); + + // cleanup + fs.unlinkSync(overwriteFile); + }); + + it("creates file when not existing using default force parameter (no overwrite)", () => { + const freshFile = path.join( + process.cwd(), + `template.default.${Date.now()}.xlsx`, + ); + expect(fs.existsSync(freshFile)).toBe(false); + const out = generateTemplateExcel(freshFile); // force omitted -> false + expect(out).toBe(freshFile); + expect(fs.existsSync(freshFile)).toBe(true); + // cleanup + fs.unlinkSync(freshFile); + }); + + it("rejects non-xlsx outputs", () => { + expect(() => generateTemplateExcel("template.csv", true)).toThrow( + /must end with .xlsx/, + ); + }); +}); diff --git a/packages/excel-parser/src/cli-parse.ts b/packages/excel-parser/src/cli-parse.ts new file mode 100644 index 0000000..989f358 --- /dev/null +++ b/packages/excel-parser/src/cli-parse.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { parseExcelFile } from "./parse-excel"; + +interface Arguments { + input: string; + output?: string; + pretty: boolean; +} + +async function main() { + const argv = await yargs(hideBin(process.argv)) + .usage("Usage: $0 [options]") + .command( + "$0 ", + "Parse an Excel file and output the parsed JSON data", + (args) => { + return args.positional("input", { + describe: "Path to the Excel file to parse", + type: "string", + demandOption: true, + }); + }, + ) + .option("output", { + alias: "o", + type: "string", + description: "Write output to a file instead of stdout", + }) + .option("pretty", { + alias: "p", + type: "boolean", + default: false, + description: "Pretty-print the JSON output", + }) + .example("$0 config.xlsx", "Parse and output to stdout") + .example("$0 config.xlsx --pretty", "Parse and pretty-print to stdout") + .example("$0 config.xlsx -o output.json", "Parse and save to file") + .example( + "$0 config.xlsx --pretty --output output.json", + "Parse with pretty formatting and save to file", + ) + .help("h") + .alias("h", "help") + .strict() + .parseAsync(); + + const { input, output, pretty } = argv as unknown as Arguments; + + // Resolve input file path + const resolvedInput = path.isAbsolute(input) + ? input + : path.join(process.cwd(), input); + + // Check if input file exists + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (!fs.existsSync(resolvedInput)) { + // eslint-disable-next-line no-console + console.error(`Error: Input file not found: ${resolvedInput}`); + process.exit(1); + } + + // Check if input file is an Excel file + if (!/\.xlsx?$/i.test(resolvedInput)) { + // eslint-disable-next-line no-console + console.error(`Error: Input file must be an Excel file (.xlsx or .xls)`); + process.exit(1); + } + + try { + // Parse the Excel file + const result = parseExcelFile(resolvedInput); + + // Format the output + const jsonOutput = pretty + ? JSON.stringify(result, null, 2) + : JSON.stringify(result); + + if (output) { + // Write to file + const resolvedOutput = path.isAbsolute(output) + ? output + : path.join(process.cwd(), output); + + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(resolvedOutput, jsonOutput, "utf8"); + // eslint-disable-next-line no-console + console.error( + `✓ Successfully parsed and wrote output to: ${resolvedOutput}`, + ); + } else { + // Write to stdout + // eslint-disable-next-line no-console + console.log(jsonOutput); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error( + `Error parsing Excel file: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } +} + +main().catch((error) => { + // eslint-disable-next-line no-console + console.error(`Unexpected error: ${error}`); + process.exit(1); +}); diff --git a/packages/excel-parser/src/cli-template.ts b/packages/excel-parser/src/cli-template.ts new file mode 100644 index 0000000..c032b2c --- /dev/null +++ b/packages/excel-parser/src/cli-template.ts @@ -0,0 +1,85 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { generateTemplateExcel } from "./template"; + +interface Arguments { + output: string; + force: boolean; +} + +async function main() { + const argv = await yargs(hideBin(process.argv)) + .usage("Usage: $0 [options]") + .command( + "$0 ", + "Generate a template Excel file with the correct sheet structure and headers", + (args) => { + return args.positional("output", { + describe: "Path to the output Excel file to create", + type: "string", + demandOption: true, + }); + }, + ) + .option("force", { + alias: "f", + type: "boolean", + default: false, + description: "Overwrite existing file if it exists", + }) + .example("$0 template.xlsx", "Generate a template file") + .example( + "$0 template.xlsx --force", + "Generate a template file, overwriting if it exists", + ) + .help("h") + .alias("h", "help") + .strict() + .parseAsync(); + + const { output, force } = argv as unknown as Arguments; + + // Resolve output file path + const resolvedOutput = path.isAbsolute(output) + ? output + : path.join(process.cwd(), output); + + // Check if output file has correct extension + if (!/\.xlsx$/i.test(resolvedOutput)) { + // eslint-disable-next-line no-console + console.error(`Error: Output file must end with .xlsx: ${resolvedOutput}`); + process.exit(1); + } + + // Check if output file exists and force is not set + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (fs.existsSync(resolvedOutput) && !force) { + // eslint-disable-next-line no-console + console.error( + `Error: Output file already exists: ${resolvedOutput}\nUse --force to overwrite`, + ); + process.exit(1); + } + + try { + // Generate the template + const result = generateTemplateExcel(resolvedOutput, force); + // eslint-disable-next-line no-console + console.log(`✓ Template successfully generated: ${result}`); + } catch (error) { + // eslint-disable-next-line no-console + console.error( + `Error generating template: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } +} + +main().catch((error) => { + // eslint-disable-next-line no-console + console.error(`Unexpected error: ${error}`); + process.exit(1); +}); diff --git a/packages/excel-parser/src/index.ts b/packages/excel-parser/src/index.ts new file mode 100644 index 0000000..24815dc --- /dev/null +++ b/packages/excel-parser/src/index.ts @@ -0,0 +1,3 @@ +export { parseExcelFile } from "./parse-excel"; +export type { ParseResult } from "./parse-excel"; +export { generateTemplateExcel } from "./template"; diff --git a/packages/excel-parser/src/parse-excel.ts b/packages/excel-parser/src/parse-excel.ts new file mode 100644 index 0000000..a0857df --- /dev/null +++ b/packages/excel-parser/src/parse-excel.ts @@ -0,0 +1,596 @@ +import * as XLSX from "xlsx"; +import { + $PackSpecification, + PackSpecification, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification"; +import { + $LetterVariant, + LetterVariant, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/letter-variant"; +import { + $VolumeGroup, + VolumeGroup, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/volume-group"; +import { + $Supplier, + Supplier, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier"; +import { + $SupplierAllocation, + SupplierAllocation, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-allocation"; +import { + $SupplierPack, + SupplierPack, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-pack"; + +interface PackSpecificationRow { + id: string; + name: string; + description?: string; + status: string; + version: string; + createdAt: string | number; + updatedAt: string | number; + billingId?: string; + // Constraints + "constraints.sheets"?: string; + "constraints.sides"?: string; + "constraints.deliveryDays"?: string; + "constraints.blackCoveragePercentage"?: string; + "constraints.colourCoveragePercentage"?: string; + // Postage (only id and size are required, plus optional fields) + "postage.id": string; + "postage.size": string; + "postage.deliveryDays"?: string; + "postage.maxWeightGrams"?: string; + "postage.maxThicknessMm"?: string; + // Assembly + "assembly.envelopeId"?: string; + "assembly.printColour"?: string; + "assembly.duplex"?: string; + "assembly.paper.id"?: string; + "assembly.paper.name"?: string; + "assembly.paper.weightGSM"?: string; + "assembly.paper.size"?: string; + "assembly.paper.colour"?: string; + "assembly.paper.finish"?: string; + "assembly.paper.recycled"?: string; + "assembly.insertIds"?: string; + "assembly.features"?: string; + "assembly.additional"?: string; +} + +interface LetterVariantRow { + id: string; + name: string; + description?: string; + volumeGroupId: string; + packSpecificationIds: string; + type: string; + status: string; + clientId?: string; + campaignIds?: string; + supplierId?: string; + // Constraints + "constraints.sheets"?: string; + "constraints.sides"?: string; + "constraints.deliveryDays"?: string; + "constraints.blackCoveragePercentage"?: string; + "constraints.colourCoveragePercentage"?: string; +} + +interface VolumeGroupRow { + id: string; + name: string; + description?: string; + startDate: string | number; + endDate?: string | number; + status?: string; // new optional status column +} + +interface SupplierRow { + id: string; + name: string; + channelType: string; + dailyCapacity: string; + status?: string; +} + +interface SupplierAllocationRow { + id: string; + volumeGroupId: string; + supplier: string; + allocationPercentage: string; + status: string; +} + +interface SupplierPackRow { + id: string; + packSpecificationId: string; + supplierId: string; + approval: string; + status: string; +} + +function parseDate(dateStr?: string | number): string { + if (!dateStr) return "2023-01-01T00:00:00Z"; + + // Handle Excel serial date numbers + if (typeof dateStr === "number") { + // Excel serial date: days since 1900-01-01 (with Excel's leap year bug) + // Excel incorrectly treats 1900 as a leap year, so subtract 1 for dates after Feb 28, 1900 + const excelEpoch = new Date(1899, 11, 30); // Dec 30, 1899 + const date = new Date(excelEpoch.getTime() + dateStr * 24 * 60 * 60 * 1000); + return date.toISOString(); + } + + const date = new Date(dateStr); + return Number.isNaN(date.getTime()) + ? "2023-01-01T00:00:00Z" + : date.toISOString(); +} + +function parseDateOnly(dateStr?: string | number): string { + if (!dateStr) return "2023-01-01"; // default date only + + // Handle Excel serial date numbers + if (typeof dateStr === "number") { + // Excel serial date: days since 1900-01-01 (with Excel's leap year bug) + const excelEpoch = new Date(1899, 11, 30); // Dec 30, 1899 + const date = new Date(excelEpoch.getTime() + dateStr * 24 * 60 * 60 * 1000); + return date.toISOString().split("T")[0]; + } + + const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) return "2023-01-01"; + // toISOString gives full timestamp, slice to date portion + return date.toISOString().split("T")[0]; +} + +function parseArray(value?: string): string[] | undefined { + if (!value || value.trim() === "") return undefined; + return value.split(",").map((item) => item.trim()); +} + +function parseConstraints(row: { + "constraints.sheets"?: string; + "constraints.sides"?: string; + "constraints.deliveryDays"?: string; + "constraints.blackCoveragePercentage"?: string; + "constraints.colourCoveragePercentage"?: string; +}): PackSpecification["constraints"] | undefined { + const constraints: NonNullable = {}; + let hasConstraints = false; + + if (row["constraints.sheets"]) { + constraints.sheets = { + value: Number.parseInt(row["constraints.sheets"], 10), + operator: "LESS_THAN", + }; + hasConstraints = true; + } + if (row["constraints.sides"]) { + constraints.sides = { + value: Number.parseInt(row["constraints.sides"], 10), + operator: "LESS_THAN", + }; + hasConstraints = true; + } + if (row["constraints.deliveryDays"]) { + constraints.deliveryDays = { + value: Number.parseInt(row["constraints.deliveryDays"], 10), + operator: "LESS_THAN", + }; + hasConstraints = true; + } + if (row["constraints.blackCoveragePercentage"]) { + constraints.blackCoveragePercentage = { + value: Number.parseFloat(row["constraints.blackCoveragePercentage"]), + operator: "LESS_THAN", + }; + hasConstraints = true; + } + if (row["constraints.colourCoveragePercentage"]) { + constraints.colourCoveragePercentage = { + value: Number.parseFloat(row["constraints.colourCoveragePercentage"]), + operator: "LESS_THAN", + }; + hasConstraints = true; + } + + return hasConstraints ? constraints : undefined; +} + +function parsePostage(row: PackSpecificationRow): PackSpecification["postage"] { + if (!row["postage.id"] || !row["postage.size"]) { + throw new Error( + `Missing required postage fields (postage.id & postage.size) for PackSpecification id '${row.id}'`, + ); + } + + const postage: PackSpecification["postage"] = { + id: row["postage.id"], + size: row["postage.size"] as PackSpecification["postage"]["size"], + }; + + if (row["postage.deliveryDays"]) { + postage.deliveryDays = Number.parseInt(row["postage.deliveryDays"], 10); + } + if (row["postage.maxWeightGrams"]) { + postage.maxWeightGrams = Number.parseFloat(row["postage.maxWeightGrams"]); + } + if (row["postage.maxThicknessMm"]) { + postage.maxThicknessMm = Number.parseFloat(row["postage.maxThicknessMm"]); + } + + return postage; +} + +function parseAssembly( + row: PackSpecificationRow, +): NonNullable | undefined { + const assembly: NonNullable = {}; + let hasAssembly = false; + + if (row["assembly.envelopeId"]) { + assembly.envelopeId = row["assembly.envelopeId"]; + hasAssembly = true; + } + + if (row["assembly.printColour"]) { + assembly.printColour = row["assembly.printColour"] as "BLACK" | "COLOUR"; + hasAssembly = true; + } + + if (row["assembly.duplex"]) { + assembly.duplex = + row["assembly.duplex"] === "true" || row["assembly.duplex"] === "TRUE"; + hasAssembly = true; + } + + // Parse paper if any paper fields are present + if (row["assembly.paper.id"]) { + assembly.paper = { + id: row["assembly.paper.id"], + name: row["assembly.paper.name"] || "", + weightGSM: Number.parseFloat(row["assembly.paper.weightGSM"] || "80"), + size: row["assembly.paper.size"] as "A5" | "A4" | "A3", + colour: (row["assembly.paper.colour"] || "WHITE") as "WHITE", + recycled: + row["assembly.paper.recycled"] === "true" || + row["assembly.paper.recycled"] === "TRUE", + }; + if (row["assembly.paper.finish"]) { + assembly.paper.finish = row["assembly.paper.finish"] as + | "MATT" + | "GLOSSY" + | "SILK"; + } + hasAssembly = true; + } + + if (row["assembly.insertIds"]) { + const insertIds = parseArray(row["assembly.insertIds"]); + if (insertIds) { + assembly.insertIds = insertIds; + hasAssembly = true; + } + } + + if (row["assembly.features"]) { + const features = parseArray(row["assembly.features"]); + if (features) { + assembly.features = features as ( + | "BRAILLE" + | "AUDIO" + | "ADMAIL" + | "SAME_DAY" + )[]; + hasAssembly = true; + } + } + + if (row["assembly.additional"]) { + try { + assembly.additional = JSON.parse(row["assembly.additional"]); + hasAssembly = true; + } catch { + // If not valid JSON, ignore it + } + } + + return hasAssembly ? assembly : undefined; +} + +function parsePackSpecification(row: PackSpecificationRow): PackSpecification { + const draft: Partial = { + id: row.id, + name: row.name, + status: row.status as PackSpecification["status"], + version: Number.parseInt(row.version, 10), + createdAt: parseDate(row.createdAt), + updatedAt: parseDate(row.updatedAt), + postage: parsePostage(row), + }; + + if (row.description) draft.description = row.description; + if (row.billingId) draft.billingId = row.billingId; + + const constraints = parseConstraints(row); + if (constraints) draft.constraints = constraints; + + const assembly = parseAssembly(row); + if (assembly) draft.assembly = assembly; + + const parsed = $PackSpecification.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for PackSpecification '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +function parseLetterVariant(row: LetterVariantRow): LetterVariant { + const baseIds = parseArray(row.packSpecificationIds) ?? []; + const draft: Partial = { + id: row.id, + name: row.name, + description: row.description || row.name, + volumeGroupId: row.volumeGroupId, + type: row.type as LetterVariant["type"], + status: row.status as LetterVariant["status"], + packSpecificationIds: baseIds, + }; + + if (row.clientId) draft.clientId = row.clientId; + if (row.campaignIds) draft.campaignIds = parseArray(row.campaignIds); + if (row.supplierId) draft.supplierId = row.supplierId; + + const constraints = parseConstraints(row); + if (constraints) draft.constraints = constraints; + + const parsed = $LetterVariant.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for LetterVariant '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +function parseVolumeGroup(row: VolumeGroupRow): VolumeGroup { + const draft: Partial = { + id: row.id, + name: row.name, + startDate: parseDateOnly(row.startDate), + status: (row.status || "DRAFT") as VolumeGroup["status"], + }; + + if (row.description) draft.description = row.description; + if (row.endDate) draft.endDate = parseDateOnly(row.endDate); + + const parsed = $VolumeGroup.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for VolumeGroup '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +function parseSupplier(row: SupplierRow): Supplier { + const draft: Partial = { + id: row.id, + name: row.name, + channelType: row.channelType as Supplier["channelType"], + dailyCapacity: Number.parseInt(row.dailyCapacity, 10), + status: (row.status || "DRAFT") as Supplier["status"], + }; + + const parsed = $Supplier.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for Supplier '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +function parseSupplierAllocation( + row: SupplierAllocationRow, +): SupplierAllocation { + const draft = { + id: row.id, + volumeGroup: row.volumeGroupId, + supplier: row.supplier, + allocationPercentage: Number.parseFloat(row.allocationPercentage), + status: row.status as SupplierAllocation["status"], + }; + + const parsed = $SupplierAllocation.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for SupplierAllocation '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +function parseSupplierPack(row: SupplierPackRow): SupplierPack { + const draft = { + id: row.id, + packSpecificationId: row.packSpecificationId, + supplierId: row.supplierId, + approval: row.approval as SupplierPack["approval"], + status: row.status as SupplierPack["status"], + }; + + const parsed = $SupplierPack.safeParse(draft); + if (!parsed.success) { + throw new Error( + `Validation failed for SupplierPack '${row.id}': ${JSON.stringify( + parsed.error.issues, + )}`, + ); + } + return parsed.data; +} + +export interface ParseResult { + packs: Record; + variants: Record; + volumeGroups: Record; + suppliers: Record; + allocations: Record; + supplierPacks: Record; +} + +function sanitizeId(id: string): string { + return id.replaceAll(/[^a-zA-Z0-9]/g, ""); +} + +function buildPacks( + packRows: PackSpecificationRow[], +): Record { + const packs: Record = {}; + for (const row of packRows) { + const pack = parsePackSpecification(row); + const key = sanitizeId(pack.id); + Object.defineProperty(packs, key, { value: pack, enumerable: true }); + } + return packs; +} + +function buildVariants( + variantRows: LetterVariantRow[], +): Record { + const variants: Record = {}; + for (const row of variantRows) { + const variant = parseLetterVariant(row); + const key = sanitizeId(variant.id); + Object.defineProperty(variants, key, { value: variant, enumerable: true }); + } + return variants; +} + +function buildVolumeGroups( + volumeGroupRows: VolumeGroupRow[], +): Record { + const volumeGroups: Record = {}; + for (const row of volumeGroupRows) { + const volumeGroup = parseVolumeGroup(row); + const key = sanitizeId(volumeGroup.id); + Object.defineProperty(volumeGroups, key, { + value: volumeGroup, + enumerable: true, + }); + } + return volumeGroups; +} + +function buildSuppliers(supplierRows: SupplierRow[]): Record { + const suppliers: Record = {}; + for (const row of supplierRows) { + const supplier = parseSupplier(row); + const key = sanitizeId(supplier.id); + Object.defineProperty(suppliers, key, { + value: supplier, + enumerable: true, + }); + } + return suppliers; +} + +function buildAllocations( + allocationRows: SupplierAllocationRow[], +): Record { + const allocations: Record = {}; + for (const row of allocationRows) { + const allocation = parseSupplierAllocation(row); + const key = sanitizeId(allocation.id); + Object.defineProperty(allocations, key, { + value: allocation, + enumerable: true, + }); + } + return allocations; +} + +function buildSupplierPacks( + supplierPackRows: SupplierPackRow[], +): Record { + const supplierPacks: Record = {}; + for (const row of supplierPackRows) { + const supplierPack = parseSupplierPack(row); + const key = sanitizeId(supplierPack.id); + Object.defineProperty(supplierPacks, key, { + value: supplierPack, + enumerable: true, + }); + } + return supplierPacks; +} + +export function parseExcelFile(filePath: string): ParseResult { + const workbook = XLSX.readFile(filePath); + + const packSheet = workbook.Sheets.PackSpecification; + if (!packSheet) + throw new Error("PackSpecification sheet not found in Excel file"); + const packRows: PackSpecificationRow[] = XLSX.utils.sheet_to_json(packSheet); + const packs = buildPacks(packRows); + + const variantSheet = workbook.Sheets.LetterVariant; + if (!variantSheet) + throw new Error("LetterVariant sheet not found in Excel file"); + const variantRows: LetterVariantRow[] = + XLSX.utils.sheet_to_json(variantSheet); + const variants = buildVariants(variantRows); + + const volumeGroupSheet = workbook.Sheets.VolumeGroup; + if (!volumeGroupSheet) + throw new Error("VolumeGroup sheet not found in Excel file"); + const volumeGroupRows: VolumeGroupRow[] = + XLSX.utils.sheet_to_json(volumeGroupSheet); + const volumeGroups = buildVolumeGroups(volumeGroupRows); + + const supplierSheet = workbook.Sheets.Supplier; + if (!supplierSheet) throw new Error("Supplier sheet not found in Excel file"); + const supplierRows: SupplierRow[] = XLSX.utils.sheet_to_json(supplierSheet); + const suppliers = buildSuppliers(supplierRows); + + const allocationSheet = workbook.Sheets.SupplierAllocation; + if (!allocationSheet) + throw new Error("SupplierAllocation sheet not found in Excel file"); + const allocationRows: SupplierAllocationRow[] = + XLSX.utils.sheet_to_json(allocationSheet); + const allocations = buildAllocations(allocationRows); + + const supplierPackSheet = workbook.Sheets.SupplierPack; + if (!supplierPackSheet) + throw new Error("SupplierPack sheet not found in Excel file"); + const supplierPackRows: SupplierPackRow[] = + XLSX.utils.sheet_to_json(supplierPackSheet); + const supplierPacks = buildSupplierPacks(supplierPackRows); + + return { + packs, + variants, + volumeGroups, + suppliers, + allocations, + supplierPacks, + }; +} diff --git a/packages/excel-parser/src/template.ts b/packages/excel-parser/src/template.ts new file mode 100644 index 0000000..6d5820f --- /dev/null +++ b/packages/excel-parser/src/template.ts @@ -0,0 +1,111 @@ +import path from "node:path"; +import fs from "node:fs"; +import * as XLSX from "xlsx"; + +export function generateTemplateExcel(out: string, force = false): string { + const resolved = path.isAbsolute(out) ? out : path.join(process.cwd(), out); + if (!/\.xlsx$/i.test(resolved)) { + throw new Error(`Output file must end with .xlsx: ${resolved}`); + } + // eslint-disable-next-line security/detect-non-literal-fs-filename + const exists = fs.existsSync(resolved); + if (exists && !force) { + throw new Error( + `Output file already exists (use --force to overwrite): ${resolved}`, + ); + } + + const wb = XLSX.utils.book_new(); + + const addSheet = (name: string, headers: string[]) => { + const sheet = XLSX.utils.aoa_to_sheet([headers]); + XLSX.utils.book_append_sheet(wb, sheet, name); + }; + + addSheet("PackSpecification", [ + "id", + "name", + "status", + "version", + "createdAt", + "updatedAt", + "billingId", + "postage.id", + "postage.size", + "postage.deliveryDays", + "postage.maxWeightGrams", + "postage.maxThicknessMm", + "constraints.sheets", + "constraints.sides", + "constraints.deliveryDays", + "constraints.blackCoveragePercentage", + "constraints.colourCoveragePercentage", + "assembly.envelopeId", + "assembly.printColour", + "assembly.duplex", + "assembly.paper.id", + "assembly.paper.name", + "assembly.paper.weightGSM", + "assembly.paper.size", + "assembly.paper.colour", + "assembly.paper.recycled", + "assembly.insertIds", + "assembly.features", + "assembly.additional", + ]); + + addSheet("LetterVariant", [ + "id", + "name", + "description", + "volumeGroupId", + "packSpecificationIds", + "type", + "status", + "clientId", + "campaignIds", + "supplierId", + "constraints.sheets", + "constraints.sides", + "constraints.deliveryDays", + "constraints.blackCoveragePercentage", + "constraints.colourCoveragePercentage", + ]); + + addSheet("VolumeGroup", [ + "id", + "name", + "description", + "startDate", + "endDate", + "status", + ]); + + addSheet("Supplier", [ + "id", + "name", + "channelType", + "dailyCapacity", + "status", + ]); + + addSheet("SupplierAllocation", [ + "id", + "volumeGroupId", + "supplier", + "allocationPercentage", + "status", + ]); + + addSheet("SupplierPack", [ + "id", + "packSpecificationId", + "supplierId", + "status", + ]); + + XLSX.writeFile(wb, resolved); + return resolved; +} + +export default generateTemplateExcel; diff --git a/packages/excel-parser/tsconfig.json b/packages/excel-parser/tsconfig.json new file mode 100644 index 0000000..295f7c2 --- /dev/null +++ b/packages/excel-parser/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "declaration": true, + "isolatedModules": true, + "module": "commonjs", + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src" + }, + "exclude": [ + "node_modules", + "dist" + ], + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*" + ] +} diff --git a/packages/schemas/jest.config.js b/packages/schemas/jest.config.js deleted file mode 100644 index 091534c..0000000 --- a/packages/schemas/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - moduleFileExtensions: ['ts', 'js', 'json', 'node'], -}; diff --git a/packages/schemas/package-lock.json b/packages/schemas/package-lock.json deleted file mode 100644 index b236f05..0000000 --- a/packages/schemas/package-lock.json +++ /dev/null @@ -1,4891 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@nhsdigital/nhs-notify-supplier-config-schemas", - "packages": { - "": { - "dependencies": { - "zod": "^4.0.5" - }, - "devDependencies": { - "@types/jest": "^30.0.0", - "@types/node": "^22.13.4", - "jest": "^30.0.4", - "ts-jest": "^29.4.0", - "ts-node": "^10.9.2", - "typescript": "^5.7.3", - "zod-mermaid": "^1.0.4" - }, - "name": "@nhsdigital/nhs-notify-supplier-config-schemas", - "version": "1.0.0" - }, - "node_modules/@ampproject/remapping": { - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "dev": true, - "engines": { - "node": ">=6.0.0" - }, - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "version": "2.3.0" - }, - "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@babel/code-frame": { - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/compat-data": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/core": { - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - }, - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/generator": { - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@babel/helper-compilation-targets": { - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "version": "7.27.2" - }, - "node_modules/@babel/helper-globals": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/helper-module-imports": { - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/helper-module-transforms": { - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0" - }, - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "version": "7.27.3" - }, - "node_modules/@babel/helper-plugin-utils": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/helper-string-parser": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/helper-validator-identifier": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/helper-validator-option": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/helpers": { - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "version": "7.27.6" - }, - "node_modules/@babel/parser": { - "bin": { - "parser": "bin/babel-parser.js" - }, - "dependencies": { - "@babel/types": "^7.28.0" - }, - "dev": true, - "engines": { - "node": ">=6.0.0" - }, - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "version": "7.8.4" - }, - "node_modules/@babel/plugin-syntax-bigint": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "dev": true, - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "version": "7.12.13" - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "version": "7.14.5" - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dev": true, - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "version": "7.10.4" - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-jsx": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dev": true, - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "version": "7.10.4" - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dev": true, - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "version": "7.10.4" - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "dev": true, - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "version": "7.8.3" - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "version": "7.14.5" - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "version": "7.14.5" - }, - "node_modules/@babel/plugin-syntax-typescript": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "version": "7.27.1" - }, - "node_modules/@babel/template": { - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "version": "7.27.2" - }, - "node_modules/@babel/traverse": { - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "version": "7.28.0" - }, - "node_modules/@babel/types": { - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "version": "7.28.1" - }, - "node_modules/@bcoe/v8-coverage": { - "dev": true, - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "version": "0.2.3" - }, - "node_modules/@cspotcode/source-map-support": { - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "version": "0.8.1" - }, - "node_modules/@emnapi/core": { - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - }, - "dev": true, - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "version": "1.4.5" - }, - "node_modules/@emnapi/runtime": { - "dependencies": { - "tslib": "^2.4.0" - }, - "dev": true, - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "version": "1.4.5" - }, - "node_modules/@emnapi/wasi-threads": { - "dependencies": { - "tslib": "^2.4.0" - }, - "dev": true, - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "version": "1.0.4" - }, - "node_modules/@isaacs/cliui": { - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "version": "8.0.2" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "version": "1.1.0" - }, - "node_modules/@istanbuljs/schema": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "version": "0.1.3" - }, - "node_modules/@jest/console": { - "dependencies": { - "@jest/types": "30.0.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "slash": "^3.0.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/core": { - "dependencies": { - "@jest/console": "30.0.4", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.2", - "jest-config": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-resolve-dependencies": "30.0.4", - "jest-runner": "30.0.4", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "jest-watcher": "30.0.4", - "micromatch": "^4.0.8", - "pretty-format": "30.0.2", - "slash": "^3.0.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==", - "license": "MIT", - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/diff-sequences": { - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jest/environment": { - "dependencies": { - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "jest-mock": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/expect": { - "dependencies": { - "expect": "30.0.4", - "jest-snapshot": "30.0.4" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/expect-utils": { - "dependencies": { - "@jest/get-type": "30.0.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/fake-timers": { - "dependencies": { - "@jest/types": "30.0.1", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/get-type": { - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jest/globals": { - "dependencies": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/types": "30.0.1", - "jest-mock": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/pattern": { - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jest/reporters": { - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==", - "license": "MIT", - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@jest/schemas": { - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jest/snapshot-utils": { - "dependencies": { - "@jest/types": "30.0.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/source-map": { - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@jest/test-result": { - "dependencies": { - "@jest/console": "30.0.4", - "@jest/types": "30.0.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/test-sequencer": { - "dependencies": { - "@jest/test-result": "30.0.4", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", - "slash": "^3.0.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/transform": { - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.0.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", - "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": "30.0.2", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@jest/types": { - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/@jridgewell/gen-mapping": { - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "dev": true, - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "version": "0.3.12" - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/@jridgewell/resolve-uri": { - "dev": true, - "engines": { - "node": ">=6.0.0" - }, - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "version": "3.1.2" - }, - "node_modules/@jridgewell/sourcemap-codec": { - "dev": true, - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "version": "1.5.4" - }, - "node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "dev": true, - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "version": "0.3.9" - }, - "node_modules/@napi-rs/wasm-runtime": { - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - }, - "dev": true, - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "version": "0.2.12" - }, - "node_modules/@pkgjs/parseargs": { - "dev": true, - "engines": { - "node": ">=14" - }, - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "version": "0.11.0" - }, - "node_modules/@pkgr/core": { - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - }, - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "version": "0.2.9" - }, - "node_modules/@sinclair/typebox": { - "dev": true, - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "version": "0.34.38" - }, - "node_modules/@sinonjs/commons": { - "dependencies": { - "type-detect": "4.0.8" - }, - "dev": true, - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "version": "3.0.1" - }, - "node_modules/@sinonjs/fake-timers": { - "dependencies": { - "@sinonjs/commons": "^3.0.1" - }, - "dev": true, - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "version": "13.0.5" - }, - "node_modules/@tsconfig/node10": { - "dev": true, - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "version": "1.0.11" - }, - "node_modules/@tsconfig/node12": { - "dev": true, - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "version": "1.0.11" - }, - "node_modules/@tsconfig/node14": { - "dev": true, - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "version": "1.0.3" - }, - "node_modules/@tsconfig/node16": { - "dev": true, - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "version": "1.0.4" - }, - "node_modules/@tybys/wasm-util": { - "dependencies": { - "tslib": "^2.4.0" - }, - "dev": true, - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "version": "0.10.0" - }, - "node_modules/@types/babel__core": { - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - }, - "dev": true, - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "version": "7.20.5" - }, - "node_modules/@types/babel__generator": { - "dependencies": { - "@babel/types": "^7.0.0" - }, - "dev": true, - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "version": "7.27.0" - }, - "node_modules/@types/babel__template": { - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - }, - "dev": true, - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "version": "7.4.4" - }, - "node_modules/@types/babel__traverse": { - "dependencies": { - "@babel/types": "^7.20.7" - }, - "dev": true, - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "version": "7.20.7" - }, - "node_modules/@types/istanbul-lib-coverage": { - "dev": true, - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "version": "2.0.6" - }, - "node_modules/@types/istanbul-lib-report": { - "dependencies": { - "@types/istanbul-lib-coverage": "*" - }, - "dev": true, - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "version": "3.0.3" - }, - "node_modules/@types/istanbul-reports": { - "dependencies": { - "@types/istanbul-lib-report": "*" - }, - "dev": true, - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "version": "3.0.4" - }, - "node_modules/@types/jest": { - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - }, - "dev": true, - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "version": "30.0.0" - }, - "node_modules/@types/node": { - "dependencies": { - "undici-types": "~6.21.0" - }, - "dev": true, - "integrity": "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.4.tgz", - "version": "22.16.4" - }, - "node_modules/@types/stack-utils": { - "dev": true, - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "version": "2.0.3" - }, - "node_modules/@types/yargs": { - "dependencies": { - "@types/yargs-parser": "*" - }, - "dev": true, - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "version": "17.0.33" - }, - "node_modules/@types/yargs-parser": { - "dev": true, - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "version": "21.0.3" - }, - "node_modules/@ungap/structured-clone": { - "dev": true, - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "version": "1.3.0" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "cpu": [ - "arm" - ], - "dev": true, - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "cpu": [ - "arm64" - ], - "dev": true, - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "cpu": [ - "arm64" - ], - "dev": true, - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "cpu": [ - "x64" - ], - "dev": true, - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "cpu": [ - "x64" - ], - "dev": true, - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "cpu": [ - "arm" - ], - "dev": true, - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "cpu": [ - "arm" - ], - "dev": true, - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "cpu": [ - "arm64" - ], - "dev": true, - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "cpu": [ - "arm64" - ], - "dev": true, - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "cpu": [ - "ppc64" - ], - "dev": true, - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "cpu": [ - "riscv64" - ], - "dev": true, - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "cpu": [ - "riscv64" - ], - "dev": true, - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "cpu": [ - "s390x" - ], - "dev": true, - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "cpu": [ - "x64" - ], - "dev": true, - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "cpu": [ - "x64" - ], - "dev": true, - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "cpu": [ - "wasm32" - ], - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "dev": true, - "engines": { - "node": ">=14.0.0" - }, - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "license": "MIT", - "optional": true, - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "cpu": [ - "arm64" - ], - "dev": true, - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "cpu": [ - "ia32" - ], - "dev": true, - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "cpu": [ - "x64" - ], - "dev": true, - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/acorn": { - "bin": { - "acorn": "bin/acorn" - }, - "dev": true, - "engines": { - "node": ">=0.4.0" - }, - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "version": "8.15.0" - }, - "node_modules/acorn-walk": { - "dependencies": { - "acorn": "^8.11.0" - }, - "dev": true, - "engines": { - "node": ">=0.4.0" - }, - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "version": "8.3.4" - }, - "node_modules/ansi-escapes": { - "dependencies": { - "type-fest": "^0.21.3" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "version": "4.3.2" - }, - "node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - }, - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "version": "6.1.0" - }, - "node_modules/ansi-styles": { - "dependencies": { - "color-convert": "^2.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - }, - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "version": "4.3.0" - }, - "node_modules/anymatch": { - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "dev": true, - "engines": { - "node": ">= 8" - }, - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "version": "3.1.3" - }, - "node_modules/arg": { - "dev": true, - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "version": "4.1.3" - }, - "node_modules/argparse": { - "dependencies": { - "sprintf-js": "~1.0.2" - }, - "dev": true, - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "version": "1.0.10" - }, - "node_modules/async": { - "dev": true, - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "version": "3.2.6" - }, - "node_modules/babel-jest": { - "dependencies": { - "@jest/transform": "30.0.4", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.11.0" - }, - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/babel-plugin-istanbul": { - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "version": "7.0.0" - }, - "node_modules/babel-plugin-jest-hoist": { - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "@types/babel__core": "^7.20.5" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/babel-preset-current-node-syntax": { - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "dev": true, - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0" - }, - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "version": "1.1.0" - }, - "node_modules/babel-preset-jest": { - "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.11.0" - }, - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/balanced-match": { - "dev": true, - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "version": "1.0.2" - }, - "node_modules/brace-expansion": { - "dependencies": { - "balanced-match": "^1.0.0" - }, - "dev": true, - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "version": "2.0.2" - }, - "node_modules/braces": { - "dependencies": { - "fill-range": "^7.1.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "version": "3.0.3" - }, - "node_modules/browserslist": { - "bin": { - "browserslist": "cli.js" - }, - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "dev": true, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "version": "4.25.1" - }, - "node_modules/bs-logger": { - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "dev": true, - "engines": { - "node": ">= 6" - }, - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "version": "0.2.6" - }, - "node_modules/bser": { - "dependencies": { - "node-int64": "^0.4.0" - }, - "dev": true, - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "version": "2.1.1" - }, - "node_modules/buffer-from": { - "dev": true, - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "version": "1.1.2" - }, - "node_modules/callsites": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "version": "3.1.0" - }, - "node_modules/camelcase": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "version": "5.3.1" - }, - "node_modules/caniuse-lite": { - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "license": "CC-BY-4.0", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "version": "1.0.30001727" - }, - "node_modules/chalk": { - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - }, - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "version": "4.1.2" - }, - "node_modules/char-regex": { - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "version": "1.0.2" - }, - "node_modules/ci-info": { - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "version": "4.3.0" - }, - "node_modules/cjs-module-lexer": { - "dev": true, - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "version": "2.1.0" - }, - "node_modules/cliui": { - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "version": "8.0.1" - }, - "node_modules/cliui/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/cliui/node_modules/emoji-regex": { - "dev": true, - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "version": "8.0.0" - }, - "node_modules/cliui/node_modules/string-width": { - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "version": "4.2.3" - }, - "node_modules/cliui/node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - }, - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "version": "7.0.0" - }, - "node_modules/co": { - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - }, - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "version": "4.6.0" - }, - "node_modules/collect-v8-coverage": { - "dev": true, - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "version": "1.0.2" - }, - "node_modules/color-convert": { - "dependencies": { - "color-name": "~1.1.4" - }, - "dev": true, - "engines": { - "node": ">=7.0.0" - }, - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "version": "2.0.1" - }, - "node_modules/color-name": { - "dev": true, - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "version": "1.1.4" - }, - "node_modules/concat-map": { - "dev": true, - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "version": "0.0.1" - }, - "node_modules/convert-source-map": { - "dev": true, - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/create-require": { - "dev": true, - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "version": "1.1.1" - }, - "node_modules/cross-spawn": { - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dev": true, - "engines": { - "node": ">= 8" - }, - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "version": "7.0.6" - }, - "node_modules/debug": { - "dependencies": { - "ms": "^2.1.3" - }, - "dev": true, - "engines": { - "node": ">=6.0" - }, - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "version": "4.4.1" - }, - "node_modules/dedent": { - "dev": true, - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "version": "1.6.0" - }, - "node_modules/deepmerge": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "version": "4.3.1" - }, - "node_modules/detect-newline": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "version": "3.1.0" - }, - "node_modules/diff": { - "dev": true, - "engines": { - "node": ">=0.3.1" - }, - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "version": "4.0.2" - }, - "node_modules/eastasianwidth": { - "dev": true, - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "version": "0.2.0" - }, - "node_modules/ejs": { - "bin": { - "ejs": "bin/cli.js" - }, - "dependencies": { - "jake": "^10.8.5" - }, - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "version": "3.1.10" - }, - "node_modules/electron-to-chromium": { - "dev": true, - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "version": "1.5.187" - }, - "node_modules/emittery": { - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - }, - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "version": "0.13.1" - }, - "node_modules/emoji-regex": { - "dev": true, - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "version": "9.2.2" - }, - "node_modules/error-ex": { - "dependencies": { - "is-arrayish": "^0.2.1" - }, - "dev": true, - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "version": "1.3.2" - }, - "node_modules/escalade": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "version": "3.2.0" - }, - "node_modules/escape-string-regexp": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/esprima": { - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "dev": true, - "engines": { - "node": ">=4" - }, - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "version": "4.0.1" - }, - "node_modules/execa": { - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - }, - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "version": "5.1.1" - }, - "node_modules/execa/node_modules/signal-exit": { - "dev": true, - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "version": "3.0.7" - }, - "node_modules/exit-x": { - "dev": true, - "engines": { - "node": ">= 0.8.0" - }, - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "version": "0.2.2" - }, - "node_modules/expect": { - "dependencies": { - "@jest/expect-utils": "30.0.4", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/fast-json-stable-stringify": { - "dev": true, - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "version": "2.1.0" - }, - "node_modules/fb-watchman": { - "dependencies": { - "bser": "2.1.1" - }, - "dev": true, - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "version": "2.0.2" - }, - "node_modules/filelist": { - "dependencies": { - "minimatch": "^5.0.1" - }, - "dev": true, - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "version": "1.0.4" - }, - "node_modules/filelist/node_modules/minimatch": { - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "version": "5.1.6" - }, - "node_modules/fill-range": { - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "version": "7.1.1" - }, - "node_modules/find-up": { - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "version": "4.1.0" - }, - "node_modules/foreground-child": { - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "version": "3.3.1" - }, - "node_modules/fs.realpath": { - "dev": true, - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "version": "1.0.0" - }, - "node_modules/fsevents": { - "dev": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - }, - "hasInstallScript": true, - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "version": "2.3.3" - }, - "node_modules/gensync": { - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "version": "1.0.0-beta.2" - }, - "node_modules/get-caller-file": { - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - }, - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "version": "2.0.5" - }, - "node_modules/get-package-type": { - "dev": true, - "engines": { - "node": ">=8.0.0" - }, - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "version": "0.1.0" - }, - "node_modules/get-stream": { - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/glob": { - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "dev": true, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "version": "10.4.5" - }, - "node_modules/graceful-fs": { - "dev": true, - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "version": "4.2.11" - }, - "node_modules/has-flag": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "version": "4.0.0" - }, - "node_modules/html-escaper": { - "dev": true, - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "version": "2.0.2" - }, - "node_modules/human-signals": { - "dev": true, - "engines": { - "node": ">=10.17.0" - }, - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "version": "2.1.0" - }, - "node_modules/import-local": { - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "version": "3.2.0" - }, - "node_modules/imurmurhash": { - "dev": true, - "engines": { - "node": ">=0.8.19" - }, - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "version": "0.1.4" - }, - "node_modules/inflight": { - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - }, - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "version": "1.0.6" - }, - "node_modules/inherits": { - "dev": true, - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "version": "2.0.4" - }, - "node_modules/is-arrayish": { - "dev": true, - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "version": "0.2.1" - }, - "node_modules/is-fullwidth-code-point": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "version": "3.0.0" - }, - "node_modules/is-generator-fn": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "version": "2.1.0" - }, - "node_modules/is-number": { - "dev": true, - "engines": { - "node": ">=0.12.0" - }, - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "version": "7.0.0" - }, - "node_modules/is-stream": { - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "version": "2.0.1" - }, - "node_modules/isexe": { - "dev": true, - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/istanbul-lib-coverage": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "version": "3.2.2" - }, - "node_modules/istanbul-lib-instrument": { - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "version": "6.0.3" - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "bin": { - "semver": "bin/semver.js" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "version": "7.7.2" - }, - "node_modules/istanbul-lib-report": { - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "version": "3.0.1" - }, - "node_modules/istanbul-lib-source-maps": { - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "version": "5.0.6" - }, - "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/istanbul-reports": { - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "version": "3.1.7" - }, - "node_modules/jackspeak": { - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "dev": true, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - }, - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "version": "3.4.3" - }, - "node_modules/jake": { - "bin": { - "jake": "bin/cli.js" - }, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "version": "10.9.2" - }, - "node_modules/jake/node_modules/brace-expansion": { - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - }, - "dev": true, - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "version": "1.1.12" - }, - "node_modules/jake/node_modules/minimatch": { - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "dev": true, - "engines": { - "node": "*" - }, - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "version": "3.1.2" - }, - "node_modules/jest": { - "bin": { - "jest": "bin/jest.js" - }, - "dependencies": { - "@jest/core": "30.0.4", - "@jest/types": "30.0.1", - "import-local": "^3.2.0", - "jest-cli": "30.0.4" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==", - "license": "MIT", - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-changed-files": { - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.0.2", - "p-limit": "^3.1.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-circus": { - "dependencies": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.0.2", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", - "p-limit": "^3.1.0", - "pretty-format": "30.0.2", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-cli": { - "bin": { - "jest": "bin/jest.js" - }, - "dependencies": { - "@jest/core": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "yargs": "^17.7.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==", - "license": "MIT", - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-config": { - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.4", - "@jest/types": "30.0.1", - "babel-jest": "30.0.4", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.0.4", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-runner": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.0.2", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==", - "license": "MIT", - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-diff": { - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "pretty-format": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-docblock": { - "dependencies": { - "detect-newline": "^3.1.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/jest-each": { - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", - "chalk": "^4.1.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-environment-node": { - "dependencies": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "jest-mock": "30.0.2", - "jest-util": "30.0.2", - "jest-validate": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-haste-map": { - "dependencies": { - "@jest/types": "30.0.1", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", - "license": "MIT", - "optionalDependencies": { - "fsevents": "^2.3.3" - }, - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-leak-detector": { - "dependencies": { - "@jest/get-type": "30.0.1", - "pretty-format": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-matcher-utils": { - "dependencies": { - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "jest-diff": "30.0.4", - "pretty-format": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-message-util": { - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.0.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-mock": { - "dependencies": { - "@jest/types": "30.0.1", - "@types/node": "*", - "jest-util": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-pnp-resolver": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "license": "MIT", - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "version": "1.2.3" - }, - "node_modules/jest-regex-util": { - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "version": "30.0.1" - }, - "node_modules/jest-resolve": { - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-resolve-dependencies": { - "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.4" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-runner": { - "dependencies": { - "@jest/console": "30.0.4", - "@jest/environment": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-leak-detector": "30.0.2", - "jest-message-util": "30.0.2", - "jest-resolve": "30.0.2", - "jest-runtime": "30.0.4", - "jest-util": "30.0.2", - "jest-watcher": "30.0.4", - "jest-worker": "30.0.2", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-runtime": { - "dependencies": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/globals": "30.0.4", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-snapshot": { - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.4", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0", - "chalk": "^4.1.2", - "expect": "30.0.4", - "graceful-fs": "^4.2.11", - "jest-diff": "30.0.4", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-snapshot/node_modules/semver": { - "bin": { - "semver": "bin/semver.js" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "version": "7.7.2" - }, - "node_modules/jest-util": { - "dependencies": { - "@jest/types": "30.0.1", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-util/node_modules/picomatch": { - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - }, - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "version": "4.0.3" - }, - "node_modules/jest-validate": { - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-validate/node_modules/camelcase": { - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "version": "6.3.0" - }, - "node_modules/jest-watcher": { - "dependencies": { - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.0.2", - "string-length": "^4.0.2" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.4.tgz", - "version": "30.0.4" - }, - "node_modules/jest-worker": { - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.2", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/jest-worker/node_modules/supports-color": { - "dependencies": { - "has-flag": "^4.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - }, - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "version": "8.1.1" - }, - "node_modules/js-tokens": { - "dev": true, - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "version": "4.0.0" - }, - "node_modules/js-yaml": { - "bin": { - "js-yaml": "bin/js-yaml.js" - }, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "dev": true, - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "version": "3.14.1" - }, - "node_modules/jsesc": { - "bin": { - "jsesc": "bin/jsesc" - }, - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "version": "3.1.0" - }, - "node_modules/json-parse-even-better-errors": { - "dev": true, - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "version": "2.3.1" - }, - "node_modules/json5": { - "bin": { - "json5": "lib/cli.js" - }, - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "version": "2.2.3" - }, - "node_modules/leven": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "version": "3.1.0" - }, - "node_modules/lines-and-columns": { - "dev": true, - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "version": "1.2.4" - }, - "node_modules/locate-path": { - "dependencies": { - "p-locate": "^4.1.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "version": "5.0.0" - }, - "node_modules/lodash.memoize": { - "dev": true, - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "version": "4.1.2" - }, - "node_modules/lru-cache": { - "dependencies": { - "yallist": "^3.0.2" - }, - "dev": true, - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "version": "5.1.1" - }, - "node_modules/make-dir": { - "dependencies": { - "semver": "^7.5.3" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "version": "4.0.0" - }, - "node_modules/make-dir/node_modules/semver": { - "bin": { - "semver": "bin/semver.js" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "version": "7.7.2" - }, - "node_modules/make-error": { - "dev": true, - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "version": "1.3.6" - }, - "node_modules/makeerror": { - "dependencies": { - "tmpl": "1.0.5" - }, - "dev": true, - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "version": "1.0.12" - }, - "node_modules/merge-stream": { - "dev": true, - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/micromatch": { - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "dev": true, - "engines": { - "node": ">=8.6" - }, - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "version": "4.0.8" - }, - "node_modules/mimic-fn": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "version": "2.1.0" - }, - "node_modules/minimatch": { - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "version": "9.0.5" - }, - "node_modules/minipass": { - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "version": "7.1.2" - }, - "node_modules/ms": { - "dev": true, - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "version": "2.1.3" - }, - "node_modules/napi-postinstall": { - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - }, - "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", - "version": "0.3.2" - }, - "node_modules/natural-compare": { - "dev": true, - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "version": "1.4.0" - }, - "node_modules/node-int64": { - "dev": true, - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "version": "0.4.0" - }, - "node_modules/node-releases": { - "dev": true, - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "version": "2.0.19" - }, - "node_modules/normalize-path": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "version": "3.0.0" - }, - "node_modules/npm-run-path": { - "dependencies": { - "path-key": "^3.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "version": "4.0.1" - }, - "node_modules/once": { - "dependencies": { - "wrappy": "1" - }, - "dev": true, - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "version": "1.4.0" - }, - "node_modules/onetime": { - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "version": "5.1.2" - }, - "node_modules/p-limit": { - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "version": "3.1.0" - }, - "node_modules/p-locate": { - "dependencies": { - "p-limit": "^2.2.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "version": "4.1.0" - }, - "node_modules/p-locate/node_modules/p-limit": { - "dependencies": { - "p-try": "^2.0.0" - }, - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "version": "2.3.0" - }, - "node_modules/p-try": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "version": "2.2.0" - }, - "node_modules/package-json-from-dist": { - "dev": true, - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "version": "1.0.1" - }, - "node_modules/parse-json": { - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "version": "5.2.0" - }, - "node_modules/path-exists": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "version": "4.0.0" - }, - "node_modules/path-is-absolute": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "version": "1.0.1" - }, - "node_modules/path-key": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "version": "3.1.1" - }, - "node_modules/path-scurry": { - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "dev": true, - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "version": "10.4.3" - }, - "node_modules/picocolors": { - "dev": true, - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "version": "1.1.1" - }, - "node_modules/picomatch": { - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - }, - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "version": "2.3.1" - }, - "node_modules/pirates": { - "dev": true, - "engines": { - "node": ">= 6" - }, - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "version": "4.0.7" - }, - "node_modules/pkg-dir": { - "dependencies": { - "find-up": "^4.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "version": "4.2.0" - }, - "node_modules/pretty-format": { - "dependencies": { - "@jest/schemas": "30.0.1", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "version": "30.0.2" - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - }, - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "version": "5.2.0" - }, - "node_modules/pure-rand": { - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "version": "7.0.1" - }, - "node_modules/react-is": { - "dev": true, - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "version": "18.3.1" - }, - "node_modules/require-directory": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "version": "2.1.1" - }, - "node_modules/resolve-cwd": { - "dependencies": { - "resolve-from": "^5.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "version": "3.0.0" - }, - "node_modules/resolve-from": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "version": "5.0.0" - }, - "node_modules/semver": { - "bin": { - "semver": "bin/semver.js" - }, - "dev": true, - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "version": "6.3.1" - }, - "node_modules/shebang-command": { - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/shebang-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "version": "3.0.0" - }, - "node_modules/signal-exit": { - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "version": "4.1.0" - }, - "node_modules/slash": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "version": "3.0.0" - }, - "node_modules/source-map": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "version": "0.6.1" - }, - "node_modules/source-map-support": { - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dev": true, - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "version": "0.5.13" - }, - "node_modules/sprintf-js": { - "dev": true, - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "version": "1.0.3" - }, - "node_modules/stack-utils": { - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "version": "2.0.6" - }, - "node_modules/string-length": { - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "version": "4.0.2" - }, - "node_modules/string-length/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/string-length/node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/string-width": { - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "version": "5.1.2" - }, - "node_modules/string-width-cjs": { - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "name": "string-width", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "version": "4.2.3" - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "dev": true, - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "version": "8.0.0" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - }, - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "version": "7.1.0" - }, - "node_modules/strip-ansi-cjs": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "name": "strip-ansi", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/strip-bom": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "version": "4.0.0" - }, - "node_modules/strip-final-newline": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "version": "2.0.0" - }, - "node_modules/strip-json-comments": { - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "version": "3.1.1" - }, - "node_modules/supports-color": { - "dependencies": { - "has-flag": "^4.0.0" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "version": "7.2.0" - }, - "node_modules/synckit": { - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "dev": true, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - }, - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "version": "0.11.11" - }, - "node_modules/test-exclude": { - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "version": "6.0.0" - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - }, - "dev": true, - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "version": "1.1.12" - }, - "node_modules/test-exclude/node_modules/glob": { - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "version": "7.2.3" - }, - "node_modules/test-exclude/node_modules/minimatch": { - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "dev": true, - "engines": { - "node": "*" - }, - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "version": "3.1.2" - }, - "node_modules/tmpl": { - "dev": true, - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "license": "BSD-3-Clause", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "version": "1.0.5" - }, - "node_modules/to-regex-range": { - "dependencies": { - "is-number": "^7.0.0" - }, - "dev": true, - "engines": { - "node": ">=8.0" - }, - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/ts-jest": { - "bin": { - "ts-jest": "cli.js" - }, - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", - "license": "MIT", - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", - "version": "29.4.0" - }, - "node_modules/ts-jest/node_modules/semver": { - "bin": { - "semver": "bin/semver.js" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "version": "7.7.2" - }, - "node_modules/ts-jest/node_modules/type-fest": { - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "version": "4.41.0" - }, - "node_modules/ts-node": { - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "dev": true, - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - }, - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "version": "10.9.2" - }, - "node_modules/tslib": { - "dev": true, - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true, - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "version": "2.8.1" - }, - "node_modules/type-detect": { - "dev": true, - "engines": { - "node": ">=4" - }, - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "version": "4.0.8" - }, - "node_modules/type-fest": { - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "version": "0.21.3" - }, - "node_modules/typescript": { - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "dev": true, - "engines": { - "node": ">=14.17" - }, - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "version": "5.8.3" - }, - "node_modules/undici-types": { - "dev": true, - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "version": "6.21.0" - }, - "node_modules/unrs-resolver": { - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "dev": true, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "hasInstallScript": true, - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "license": "MIT", - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - }, - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "version": "1.11.1" - }, - "node_modules/update-browserslist-db": { - "bin": { - "update-browserslist-db": "cli.js" - }, - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "license": "MIT", - "peerDependencies": { - "browserslist": ">= 4.21.0" - }, - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "version": "1.1.3" - }, - "node_modules/v8-compile-cache-lib": { - "dev": true, - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "version": "3.0.1" - }, - "node_modules/v8-to-istanbul": { - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "dev": true, - "engines": { - "node": ">=10.12.0" - }, - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "version": "9.3.0" - }, - "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dev": true, - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "version": "0.3.29" - }, - "node_modules/walker": { - "dependencies": { - "makeerror": "1.0.12" - }, - "dev": true, - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "license": "Apache-2.0", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "version": "1.0.8" - }, - "node_modules/which": { - "bin": { - "node-which": "bin/node-which" - }, - "dependencies": { - "isexe": "^2.0.0" - }, - "dev": true, - "engines": { - "node": ">= 8" - }, - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "version": "2.0.2" - }, - "node_modules/wrap-ansi": { - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - }, - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "version": "8.1.0" - }, - "node_modules/wrap-ansi-cjs": { - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - }, - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "name": "wrap-ansi", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "version": "7.0.0" - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "dev": true, - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "version": "8.0.0" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "version": "4.2.3" - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - }, - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "version": "6.2.1" - }, - "node_modules/wrappy": { - "dev": true, - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "version": "1.0.2" - }, - "node_modules/write-file-atomic": { - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/y18n": { - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "version": "5.0.8" - }, - "node_modules/yallist": { - "dev": true, - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "version": "3.1.1" - }, - "node_modules/yargs": { - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "version": "17.7.2" - }, - "node_modules/yargs-parser": { - "dev": true, - "engines": { - "node": ">=12" - }, - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "version": "21.1.1" - }, - "node_modules/yargs/node_modules/ansi-regex": { - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "version": "5.0.1" - }, - "node_modules/yargs/node_modules/emoji-regex": { - "dev": true, - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "version": "8.0.0" - }, - "node_modules/yargs/node_modules/string-width": { - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "version": "4.2.3" - }, - "node_modules/yargs/node_modules/strip-ansi": { - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "dev": true, - "engines": { - "node": ">=8" - }, - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "version": "6.0.1" - }, - "node_modules/yn": { - "dev": true, - "engines": { - "node": ">=6" - }, - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "version": "3.1.1" - }, - "node_modules/yocto-queue": { - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "version": "0.1.0" - }, - "node_modules/zod": { - "funding": { - "url": "https://github.com/sponsors/colinhacks" - }, - "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", - "version": "4.0.5" - }, - "node_modules/zod-mermaid": { - "dependencies": { - "zod": "^4.0.5" - }, - "dev": true, - "engines": { - "node": ">=18.0.0" - }, - "integrity": "sha512-H3hbSoScS/1tdlKKGTzb1gQRqnlsxF3QoD4dRAqFSAfVC8/8lGDdUx+YxiXzIB6HPo1L4xjNJOPSwXxCyFTCFQ==", - "license": "MIT", - "resolved": "https://registry.npmjs.org/zod-mermaid/-/zod-mermaid-1.0.9.tgz", - "version": "1.0.9" - } - }, - "requires": true, - "version": "1.0.0" -} diff --git a/packages/schemas/src/cli/generate-json.ts b/packages/schemas/src/cli/generate-json.ts deleted file mode 100644 index 17088d5..0000000 --- a/packages/schemas/src/cli/generate-json.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod'; -import * as fs from 'node:fs'; -import packageJson from '../../package.json'; - -const version = packageJson.version; - -for (const [key, schema] of Object.entries({ - // 'client-changed': $ClientChangedEvent -})) { - const json = z.toJSONSchema(schema); - const file = `json/${key}-${version}.json`; - fs.writeFileSync(file, JSON.stringify(json, null, 2)); - console.info(`Wrote JSON schema for ${key} to ${file}`); -} diff --git a/packages/schemas/src/domain/__tests__/specification-supplier.test.ts b/packages/schemas/src/domain/__tests__/specification-supplier.test.ts deleted file mode 100644 index 12a4edd..0000000 --- a/packages/schemas/src/domain/__tests__/specification-supplier.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { $Version } from '../common'; -import { $SpecificationSupplier, SpecificationSupplier } from '../specification-supplier'; -import { EnvelopeId, Specification } from '../specification'; -import { LayoutId } from '../layout'; - -describe('SpecificationSupplier schema validation', () => { - - const standardLetterSpecification: Specification = { - id: 'standard-letter' as any, - name: 'Standard Economy-class Letter', - status: 'PUBLISHED', - createdAt: new Date(), - updatedAt: new Date(), - version: $Version.parse('1.0.0'), - layout: 'standard' as LayoutId, - postage: { - tariff: 'economy', - size: 'letter', - deliverySLA: 4, - maxSheets: 5 - }, - pack: { - envelope: 'nhs-economy' as EnvelopeId, - printColour: 'BLACK', - features: ['MAILMARK'] - } - }; - - const testSpecificationSupplier: SpecificationSupplier = { - id: 'test-specification-supplier' as any, - specificationId: standardLetterSpecification.id, - supplierId: 'supplier-123' as any, - } - - it('should validate a specification supplier', () => { - expect(() => $SpecificationSupplier.parse(testSpecificationSupplier)).not.toThrow(); - }); - -}); diff --git a/packages/schemas/src/domain/__tests__/specification.test.ts b/packages/schemas/src/domain/__tests__/specification.test.ts deleted file mode 100644 index b89a162..0000000 --- a/packages/schemas/src/domain/__tests__/specification.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { $Specification, EnvelopeId, Specification, SpecificationId } from '../specification'; -import { $Version } from '../common'; -import { LayoutId } from '../layout'; - -describe('Specification schema validation', () => { - - const standardLetterSpecification: Specification = { - id: 'standard-letter' as SpecificationId, - name: 'Standard Economy-class Letter', - status: 'PUBLISHED', - createdAt: new Date(), - updatedAt: new Date(), - version: $Version.parse('1.0.0'), - layout: 'standard' as LayoutId, - postage: { - tariff: 'economy', - size: 'letter', - deliverySLA: 4, - maxSheets: 5 - }, - pack: { - envelope: 'nhs-economy' as EnvelopeId, - printColour: 'BLACK', - features: ['MAILMARK'] - } - }; - - it('should validate a standard letter specification', () => { - expect(() => $Specification.strict().parse(standardLetterSpecification)).not.toThrow(); - }); - - it('should accept a letter specification with unrecognised fields', () => { - expect(() => $Specification.parse({ - ...standardLetterSpecification, - additionalField: { some: 'data' } - })).not.toThrow(); - }); - -}); diff --git a/packages/schemas/src/domain/channel-supplier.ts b/packages/schemas/src/domain/channel-supplier.ts deleted file mode 100644 index 3c9003c..0000000 --- a/packages/schemas/src/domain/channel-supplier.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod'; -import { $ChannelType } from './channel'; -import { ConfigBase } from './common'; - -export const $ChannelSupplier = ConfigBase('ChannelSupplier').extend({ - channelType: $ChannelType, - outputQueue: z.string(), -}).describe('ChannelSupplier'); - -export type ChannelSupplier = z.infer; diff --git a/packages/schemas/src/domain/channel.ts b/packages/schemas/src/domain/channel.ts deleted file mode 100644 index eb6e3ed..0000000 --- a/packages/schemas/src/domain/channel.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'zod'; - -export const $ChannelType = z.enum(['NHSAPP', 'SMS', 'EMAIL', 'LETTER']); - -export type ChannelType = z.infer; diff --git a/packages/schemas/src/domain/common.ts b/packages/schemas/src/domain/common.ts deleted file mode 100644 index 8116e55..0000000 --- a/packages/schemas/src/domain/common.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod'; - -export function ConfigBase(type: T) { - return z.object({ - id: z.string().brand(type), - }); -} - -export const $Version = z.string().regex(/^[0-9]+\.[0-9]+\.[0-9]+$/).brand('Version'); -export type Version = z.infer; diff --git a/packages/schemas/src/domain/index.ts b/packages/schemas/src/domain/index.ts deleted file mode 100644 index 6d5e03c..0000000 --- a/packages/schemas/src/domain/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { GovuknotifyAccount } from './govuknotify-account'; -export { $ActivePeriod, Schedule, SupplierQuota, $SupplierQuota } from './supplier-quota'; -export { ChannelSupplier, $ChannelSupplier } from './channel-supplier'; diff --git a/packages/schemas/src/domain/layout.ts b/packages/schemas/src/domain/layout.ts deleted file mode 100644 index 6443f0e..0000000 --- a/packages/schemas/src/domain/layout.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; -import { ConfigBase } from './common'; - -export const $Layout = ConfigBase('Layout').extend({ - name: z.string(), - contentBlocks: z.array(z.object({ - id: z.string(), - description: z.string().optional(), - type: z.enum(['TEXT', 'IMAGE']), - maxLength: z.number(), - })), -}).describe('Layout'); - -export type Layout = z.infer; -export type LayoutId = Layout['id']; diff --git a/packages/schemas/src/domain/specification-supplier.ts b/packages/schemas/src/domain/specification-supplier.ts deleted file mode 100644 index c3cea2e..0000000 --- a/packages/schemas/src/domain/specification-supplier.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from 'zod'; -import { idRef } from '../helpers/id-ref'; -import { $ChannelSupplier } from './channel-supplier'; -import { $Specification } from './specification'; -import { ConfigBase } from './common'; - -export const $SpecificationSupplier = ConfigBase('SpecificationSupplier').extend({ - specificationId: idRef($Specification), - supplierId: idRef($ChannelSupplier), -}).describe('SpecificationSupplier'); - -export type SpecificationSupplier = z.infer; diff --git a/packages/schemas/src/domain/specification.ts b/packages/schemas/src/domain/specification.ts deleted file mode 100644 index dd87d19..0000000 --- a/packages/schemas/src/domain/specification.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { z } from 'zod'; -import { $Version, ConfigBase } from './common'; -import { idRef } from '../helpers/id-ref'; -import { $Layout } from './layout'; - -export const $SpecificationFeature = z.enum(['SAME_DAY', 'BRAILLE', 'AUDIO_CD', 'MAILMARK']); -export const $EnvelopeFeature = z.enum(['WHITEMAIL', 'NHS_BRANDING', 'NHS_BARCODE']); - -export const $Envelope = ConfigBase('Envelope').extend({ - name: z.string(), - size: z.enum(['C5', 'C4', 'DL']), - features: z.array($EnvelopeFeature).optional(), -}).describe('Envelope'); -export type Envelope = z.infer; -export type EnvelopeId = Envelope['id']; - -export const $Insert = ConfigBase('Insert').extend({ - name: z.string(), - type: z.enum(['FLYER', 'BOOKLET']), - source: z.enum(['IN_HOUSE', 'EXTERNAL']), - artwork: z.url().optional() -}).describe('Insert'); -export type Insert = z.infer; -export type InsertId = Insert['id']; - -export const $Specification = ConfigBase('Specification').extend({ - name: z.string(), - status: z.enum(['DRAFT', 'PUBLISHED']), - createdAt: z.date(), - updatedAt: z.date(), - version: $Version, - layout: idRef($Layout), - billing: z.object({ - basePrice: z.number(), - unitPrice: z.number(), - }).partial().optional(), - postage: z.object({ - tariff: z.string(), - size: z.string(), - deliverySLA: z.number(), - maxSheets: z.number(), - maxWeight: z.number().optional(), - maxThickness: z.number().optional(), - }), - pack: z.object({ - envelope: idRef($Envelope), - printColour: z.enum(['BLACK', 'COLOUR']), - paperColour: z.string().optional(), - insert: idRef($Insert).optional(), - features: z.array($SpecificationFeature).optional(), - additional: z.record(z.string(), z.string()).optional() - }) -}).describe('Specification'); -export type Specification = z.infer; -export type SpecificationId = Specification['id']; - -export const $SpecificationGroup = ConfigBase('SpecificationGroup').extend({ - name: z.string(), - description: z.string().optional(), - specifications: z.array(idRef($Specification)).nonempty(), -}).describe('SpecificationGroup'); -export type SpecificationGroup = z.infer; diff --git a/packages/schemas/src/domain/supplier-quota.ts b/packages/schemas/src/domain/supplier-quota.ts deleted file mode 100644 index 96a9e88..0000000 --- a/packages/schemas/src/domain/supplier-quota.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from 'zod'; -import { idRef } from '../helpers/id-ref'; -import { $Queue } from './queue'; -import { $ChannelSupplier } from './channel-supplier'; -import { ConfigBase } from './common'; - -export const $DayOfWeek = z.enum([ - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat', - 'Sun', -]); - -export const $ActivePeriod = z.object({ - days: z.array($DayOfWeek), - startTimePeriod: z.string(), // HH:MM:SS locale - endTimePeriod: z.string(), // HH:MM:SS locale -}); -export type ActivePeriod = z.infer; - -export const $Schedule = z.object({ - activePeriods: z.array($ActivePeriod), -}); - -export type Schedule = z.infer; - -export const $SupplierQuota = ConfigBase('SupplierQuota').extend({ - channelSupplierId: idRef($ChannelSupplier), - inputQueueIds: z.array(idRef($Queue)), - tps: z.preprocess((val) => { - if (typeof val === 'string') { - return Number.parseInt(val); - } - return val; - }, z.number()), - periodSeconds: z.number(), - initialQuota: z.number(), - priority: z.number(), - weight: z.number(), - schedule: $Schedule.optional(), -}).describe('SupplierQuota'); - -export type SupplierQuota = z.infer; diff --git a/packages/schemas/src/schemas/base-metadata-schemas.ts b/packages/schemas/src/schemas/base-metadata-schemas.ts deleted file mode 100644 index 6f666bc..0000000 --- a/packages/schemas/src/schemas/base-metadata-schemas.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; - -const $EventMetadataVersionInformation = z.strictObject({ - dataschema: z.string(), - dataschemaversion: z.string(), - type: z.string(), -}); -export type EventMetadataVersionInformation = z.infer< - typeof $EventMetadataVersionInformation ->; - -export const $EventMetadata = $EventMetadataVersionInformation.extend({ - id: z.string(), - source: z.string(), - specversion: z.literal('1.0'), - subject: z.string(), - time: z.string(), - datacontenttype: z.literal('application/json'), - plane: z.literal('control'), -}); -export type EventMetadata = z.infer; diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json deleted file mode 100644 index 4889c6c..0000000 --- a/packages/schemas/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "outDir": "dist", - "resolveJsonModule": true, - "rootDir": "src", - "skipLibCheck": true, - "strict": true, - "target": "es2020" - } -} diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index ac566f1..54a8959 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -15,6 +15,7 @@ Git[Hh]ub Gitleaks Grype Jira +monorepo npm Podman Python diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..b340a8f --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "module": "ES2020", + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "extends": "@tsconfig/node22/tsconfig.json" +}