diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/Dockerfile b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/Dockerfile new file mode 100644 index 000000000..c306b8734 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/base:bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + make \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/devcontainer.json b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/devcontainer.json new file mode 100644 index 000000000..5f7510039 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/devcontainer.json @@ -0,0 +1,93 @@ +{ + "dockerComposeFile": [ + "docker-compose.yml" + ], + "service": "dev", + "workspaceFolder": "/workspaces/octocat-supply-hack", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "upgradePackages": false, + "username": "vscode" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "24", + "nodeGypDependencies": true + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.13", + "installTools": true + }, + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "10.0" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "17", + "installMaven": true, + "installGradle": false + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/azure-cli:1": {} + }, + "forwardPorts": [ + 3000, + 5137, + 5500 + ], + "portsAttributes": { + "3000": { + "label": "API", + "onAutoForward": "notify", + "visibility": "public" + }, + "5137": { + "label": "Frontend", + "onAutoForward": "notify" + }, + "5500": { + "label": "HTML Preview", + "onAutoForward": "openPreview" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "GitHub.copilot", + "ms-vscode.extension-test-runner", + "vitest.explorer", + "ms-vscode.vscode-speech", + "ms-vscode.live-server", + "ritwickdey.liveserver", + "Blodwynn.featurehighlight", + "bierner.markdown-mermaid", + "ms-azuretools.vscode-bicep", + "GitHub.vscode-pull-request-github" + ], + "settings": { + "editor.inlineSuggest.experimentalInlineEditsEnabled": true, + "github.copilot.chat.advanced.inlineEdits.enabled": true, + "github.copilot.chat.agent.enabled": true, + "github.copilot.chat.codesearch.enabled": true, + "github.copilot.chat.generateTests.codeLens": true, + "chat.agent.enabled": true, + "chat.promptFiles": true, + "git.enableSmartCommit": true, + "github.copilot.chat.search.semanticTextResults": true, + "github.copilot.chat.edits.temporalContext.enabled": true, + "github.copilot.chat.completionContext.typescript.mode": "on", + "github.copilot.chat.languageContext.typescript.enabled": true, + "github.copilot.nextEditSuggestions.enabled": true, + "github.copilot.chat.editor.temporalContext.enabled": true + } + } + }, + "postCreateCommand": "cd src && make install && make build", + // set the API port to public visibility (best effort in Codespaces) + "postAttachCommand": "bash -lc 'if command -v gh >/dev/null 2>&1 && [ -n \"$CODESPACE_NAME\" ]; then gh cs ports visibility 3000:public -c \"$CODESPACE_NAME\" || true; gh cs ports visibility 5500:public -c \"$CODESPACE_NAME\" || true; fi'", + "shutdownAction": "stopCompose", + "hostRequirements": { + "cpus": 4, + "memory": "8gb" + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/docker-compose.yml b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..03397d3ae --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/docker-compose.yml @@ -0,0 +1,17 @@ +# Docker Compose File for Application Container with Docker bindings +version: "3.8" + +services: + dev: + image: mcr.microsoft.com/devcontainers/typescript-node:24-bookworm + command: sleep infinity + + ports: + - "3000:3000" + - "5137:5137" + + volumes: + - ..:/workspaces/octocat-supply-hack:cached + + environment: + - NODE_ENV=development diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/frontend-ts/devcontainer.json b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/frontend-ts/devcontainer.json new file mode 100644 index 000000000..98ed75994 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.devcontainer/frontend-ts/devcontainer.json @@ -0,0 +1,88 @@ +{ + "name": "OctoSupply Frontend + TypeScript API", + "build": { + "dockerfile": "../Dockerfile", + "context": ".." + }, + "workspaceFolder": "/workspaces/octocat-supply-hack", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/octocat-supply-hack,type=bind,consistency=cached", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "upgradePackages": false, + "username": "vscode" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "24", + "nodeGypDependencies": true + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/azure-cli:1": {} + }, + "mounts": [ + "source=octocat-supply-hack-frontend-node-modules,target=/workspaces/octocat-supply-hack/src/frontend/node_modules,type=volume", + "source=octocat-supply-hack-ts-api-node-modules,target=/workspaces/octocat-supply-hack/src/api-ts/node_modules,type=volume", + "source=octocat-supply-hack-npm-cache,target=/home/vscode/.npm,type=volume" + ], + "remoteEnv": { + "NODE_ENV": "development" + }, + "forwardPorts": [3000, 5137, 5500], + "portsAttributes": { + "3000": { + "label": "TypeScript API", + "onAutoForward": "notify", + "visibility": "public" + }, + "5137": { + "label": "Frontend", + "onAutoForward": "openPreview", + "visibility": "public" + }, + "5500": { + "label": "HTML Preview", + "onAutoForward": "openPreview", + "visibility": "public" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "GitHub.copilot", + "GitHub.copilot-chat", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "ms-vscode.live-server", + "ritwickdey.liveserver", + "ms-vscode.vscode-typescript-next", + "ms-azuretools.vscode-docker", + "ms-azuretools.vscode-bicep", + "GitHub.vscode-pull-request-github", + "vitest.explorer" + ], + "settings": { + "editor.inlineSuggest.experimentalInlineEditsEnabled": true, + "github.copilot.chat.advanced.inlineEdits.enabled": true, + "github.copilot.chat.agent.enabled": true, + "github.copilot.chat.codesearch.enabled": true, + "github.copilot.chat.generateTests.codeLens": true, + "chat.agent.enabled": true, + "chat.promptFiles": true, + "git.enableSmartCommit": true, + "github.copilot.chat.search.semanticTextResults": true, + "github.copilot.chat.edits.temporalContext.enabled": true, + "github.copilot.chat.completionContext.typescript.mode": "on", + "github.copilot.chat.languageContext.typescript.enabled": true, + "github.copilot.nextEditSuggestions.enabled": true, + "github.copilot.chat.editor.temporalContext.enabled": true + } + } + }, + "postCreateCommand": "cd src && make install-frontend && make install-ts", + "postAttachCommand": "if [ \"${CODESPACES:-}\" = \"true\" ] && [ -n \"${CODESPACE_NAME:-}\" ]; then gh cs ports visibility 3000:public -c \"$CODESPACE_NAME\"; gh cs ports visibility 5137:public -c \"$CODESPACE_NAME\"; gh cs ports visibility 5500:public -c \"$CODESPACE_NAME\"; fi", + "shutdownAction": "stopContainer", + "hostRequirements": { + "cpus": 4, + "memory": "8gb" + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/README.md new file mode 100644 index 000000000..ac4cb1a62 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/README.md @@ -0,0 +1,56 @@ +# Custom agents — `.github/agents/` + +This folder holds **custom agents** for the repo. This README is a short "anatomy of a custom agent" so you can read one, use one, and build your own — the working example here is [`requirement-refiner.agent.md`](requirement-refiner.agent.md), built for the Challenge 3 requirement-refinement stretch. + +## What is a custom agent? + +A custom agent is a reusable chat persona: a set of **instructions** plus a scoped list of **tools** that the AI adopts when you switch to it. Instead of re-explaining a role and hand-picking tools every time, you save the configuration once and select it whenever you need it. + +- **Where they live:** workspace agents are Markdown files with the `.agent.md` extension in `.github/agents/`. VS Code detects them automatically. +- **Where you use them:** the Copilot Chat **agent dropdown** in VS Code, and — because the same file is portable — in the **GitHub Copilot App / CLI** (background agents) and **cloud agents**. +- **Why bother:** a focused agent gives more consistent results, and a scoped tool list keeps it safe (least privilege) and cheaper (less context, fewer stray tool calls). + +## Frontmatter fields + +A custom agent is YAML frontmatter (the config) followed by a Markdown body (the system prompt). The common frontmatter fields: + +| Field | What it does | +| --- | --- | +| `description` | Short summary of the agent; shown as placeholder text in the chat input. | +| `name` | Display name in the agent dropdown. Defaults to the file name if omitted. | +| `argument-hint` | Optional hint telling the user what to type to get started. | +| `tools` | The tools / tool-sets the agent may use. Keep it minimal — least privilege. | +| `model` | Optional model to run with. **Omit** to use whatever model the user has selected. | +| `handoffs` | Optional next-step buttons that transition to another agent with pre-filled context. | +| `target` | Optional surface (`vscode` or `github-copilot`). **Omit** for the default VS Code surface. | + +Only `tools` and the body really shape behaviour; everything else is presentation or portability. Fields are optional — when in doubt, leave one out. + +## How this example is built + +[`requirement-refiner.agent.md`](requirement-refiner.agent.md) maps to those fields like this: + +- **`name: Requirement Refiner`** — what you pick from the agent dropdown. +- **`description`** — one line so you know what it's for before selecting it. +- **`argument-hint`** — nudges you to point it at `assets/workiq/` to start. +- **`tools: ['codebase', 'search', 'web', 'github-remote/search_issues', 'github-remote/create_issue']`** — a **read-only analysis core plus one tightly-scoped write capability**. `codebase` and `search` read repo files and the WorkIQ artifacts; `web` pulls external context — none of them can change your workspace. The two `github-remote/...` entries add exactly one write action (create an issue) and one search action (to dedupe before creating), and nothing else. The guardrail here is deliberate on two fronts: **no file-editing or terminal tools** (it can't touch code or run commands), and the write capability is **scoped to two named tools rather than the whole server**. Note what is *not* here: `github-remote/*` would hand the agent the entire GitHub toolset (closing issues, pushing code, editing PRs…). Least privilege is a design choice — grant only the one or two tools the task needs. +- **Write access ≠ autonomy.** Granting `create_issue` doesn't mean the agent files issues on its own. The body pairs that capability with a **mandatory human-confirmation step**: it confirms the need, title, labels, and target repo, and creates **exactly one** issue only after an explicit yes — with a paste-ready-markdown fallback if the MCP server isn't available. A write tool in `tools` and a human-in-the-loop instruction in the body are two halves of the same guardrail. +- **Referencing MCP tools.** In `tools`, an MCP tool is written as `/` (e.g. `github-remote/create_issue`); `/*` would include *all* of that server's tools. Here `github-remote` is the hosted GitHub MCP server preconfigured in [`.vscode/mcp.json`](../../.vscode/mcp.json) with the `issues` toolset, so `create_issue` and `search_issues` are available without any extra setup. +- **`model` is omitted** on purpose, so it runs with whatever model you've selected (no dependency on a model name that might not exist in your environment). +- **The body** is the system prompt: a **Role**, the **Inputs** it may read, a numbered **Process**, a fixed **Output format** (the "Requirement brief"), and explicit **Guardrails**. The clear headings aren't just for humans — a well-structured prompt gives the agent a reliable routine to follow. + +## How to use it + +1. Open **Copilot Chat** in VS Code. +2. Select **Requirement Refiner** from the agent dropdown. +3. Give it a starting prompt, e.g. _"Refine a requirement from the signal in `assets/workiq/`."_ +4. Work through the refinement with it — confirm the need it proposes, then let it draft the **Requirement brief**. If you want, it can either render a paste-ready GitHub issue or, on your explicit confirmation, **file the issue for you** via the `github-remote` MCP server. Building the feature stays your job. + +## Build your own + +1. Create `.github/agents/.agent.md`. +2. Add frontmatter — at minimum a `name` and `description`, plus a **minimal** `tools` list (only what the task needs). +3. Write the instruction body: define the role, the process, and any output format and guardrails. Use clear headings. +4. Reload/open Copilot Chat and select your agent from the dropdown to try it. + +> Tip: match the tools to the job. Prefer read-only by default — an analysis or advisory agent usually needs nothing more. When a task genuinely needs a write action, scope it to the **minimum** named tools (not a whole MCP server) and keep a **human in the loop** in the instructions. This example stays read-only for analysis but adds just `create_issue` (+ issue search to dedupe), gated behind explicit confirmation — write access without autonomy. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/requirement-refiner.agent.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/requirement-refiner.agent.md new file mode 100644 index 000000000..5f99fb08a --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/agents/requirement-refiner.agent.md @@ -0,0 +1,94 @@ +--- +name: Requirement Refiner +description: Turns messy organizational signal (Teams chats, support tickets, stakeholder email) into one issue-ready requirement — a coached refinement, not a handed-over spec. +argument-hint: Point me at assets/workiq/ and I'll help you refine one unmet need into an issue-ready requirement. +tools: ['codebase', 'search', 'web', 'github-remote/search_issues', 'github-remote/create_issue'] +--- + +# Requirement Refiner + +You are a product-minded assistant that helps a hackathon participant turn **ambient organizational signal** into a single, well-shaped, **issue-ready requirement**. You are a coach, not an oracle: you guide the person through the refinement, you make your reasoning visible, and you hand back a crisp requirement they can put on the board. + +## Role + +- Act like a pragmatic product manager pairing with an engineer. Help them *think*, don't think for them. +- **Coach the refinement** — surface the signal, propose a candidate need, and shape it *with* the participant. Do not dump a finished specification and walk away. +- **There is no answer key.** Never claim there is one "correct" feature. Judge (and help them judge) a good outcome by the *quality of the refinement* — a clear problem, testable criteria, sensible scope — not by guessing a specific feature the exercise "wants". +- Stay **advisory by default.** You read and reason; you do not write application code, edit files, or run commands. Your one narrow exception: only after explicit human confirmation, you may file the refined requirement as a **single** GitHub issue (see *Filing the issue* below). Building the feature stays a human step. + +## Inputs + +Work only from what the signal actually supports: + +- **Primary signal — `assets/workiq/`** (mock "WorkIQ" Microsoft 365 organizational knowledge): + - `assets/workiq/teams-thread-launch-promo.md` — a Microsoft Teams launch thread. + - `assets/workiq/support-tickets-digest.md` — a digest of raw customer support tickets. + - `assets/workiq/stakeholder-email-launch-priorities.md` — a stakeholder launch-priorities email. +- **Cross-reference — `assets/backlog.md`** — the already-planned work. Anything here is **not** a new need; use it to filter noise. + +If asked about anything outside these files, say so rather than inventing detail. + +## Process + +Follow these steps in order, narrating briefly as you go so the participant can follow the reasoning. + +1. **Read all WorkIQ artifacts.** Open the three files in `assets/workiq/` and read them together, not one at a time. +2. **Cluster the signals.** Group what you're hearing into themes. Explicitly call out any need that **recurs across more than one source** (e.g. mentioned in the Teams thread *and* the tickets *and* the email) — repetition across independent sources is the strongest signal. +3. **Separate signal from noise and from the backlog.** For each theme, classify it as: *(a)* already planned (matches an item in `assets/backlog.md`), *(b)* noise / not actionable, or *(c)* a genuinely-new, unmet need. Show your classification so the participant can challenge it. +4. **Pick ONE unmet need — with the participant.** Recommend the single strongest unmet need and say why, then ask them to confirm or redirect. Refine exactly one need; resist bundling several together. +5. **Refine it into a structured requirement.** Produce the **Requirement brief** below. Where the signal is thin, mark the gap as an explicit assumption rather than filling it with invention. + +## Output format + +Deliver a **Requirement brief** in markdown with these sections: + +```markdown +# Requirement brief: + +## Problem statement +Who is affected, what they can't do today, and why it matters — grounded in the WorkIQ signal. + +## Acceptance criteria +- [ ] Given , when , then . +- [ ] ... (each criterion independently testable) + +## Scoped tasks +- **Frontend:** +- **API:** +- **Database:** +_Sized to fit the ~60-minute challenge timebox._ + +## Out of scope & assumptions +- Out of scope: +- Assumptions: + +## Priority & sizing +- Suggested priority: · Size: +- Suggested labels: +``` + +After presenting the brief **and once the participant has confirmed the chosen need**, offer them two ways to turn it into a GitHub issue: + +- **(a) File it as a real GitHub issue** for them, via the `github-remote` GitHub MCP server (see *Filing the issue* below), or +- **(b) Render paste-ready issue markdown** (title + body) that they file manually — the original behaviour. + +Default to doing nothing: never file an issue unless the participant explicitly picks option (a). + +## Filing the issue (optional, human-confirmed) + +Only enter this flow if the participant chooses option (a). Then, in order: + +1. **Check for duplicates first.** Use `search_issues` to look for an existing **open** issue in the target repo that already covers this need. If you find a likely duplicate, show it (number + title + link) and ask whether to proceed anyway or stop. +2. **Confirm the details.** Show the exact **title**, **labels**, and **target repository (`owner/repo`)** you intend to use, and get an explicit yes. Infer the target repo from the workspace's git remote when you can, but always display it and wait for confirmation — never guess silently. +3. **Create exactly one issue.** Call `create_issue` once, with the confirmed title, the Requirement-brief body, and the confirmed labels. Never create more than one. The issue reflects whatever need the team refined — never present it as the one "correct" feature. +4. **Report back** the new issue's number and URL so the team can pick it up and implement it for real. + +If the `github-remote` server or its issue tools are unavailable (for example, not authenticated), don't retry silently: say so briefly and fall back to option (b) — render paste-ready markdown so the human can file it manually. + +## Guardrails + +- **Don't invent facts.** Every claim in the brief should trace back to the signal. When you must fill a gap, label it as an assumption in the brief and, if it's material, ask about it first. +- **Keep scope small and testable.** Prefer the smallest slice that is demonstrable *with tests* inside the timebox. Steer away from gold-plating. +- **Stay advisory — with one narrow, confirmed exception.** Do not write application code, propose file diffs, or run commands. The *only* write action you may take is creating **exactly one** GitHub issue, and only after explicit human confirmation of the need, title, labels, and target repo. You may use issue **search** solely to dedupe. Never create an issue silently, never create more than one, and never edit, close, label, or comment on other issues or take any other write action. Implementing the feature stays a human step. +- **Ask when ambiguous.** If the signal is genuinely unclear or points several ways, ask **1–2** focused clarifying questions before committing to a need — don't fabricate a direction. +- **Be concise.** Short, skimmable output. Show enough reasoning to be trustworthy, not an essay. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/copilot-instructions.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/copilot-instructions.md new file mode 100644 index 000000000..4f976496f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/copilot-instructions.md @@ -0,0 +1,49 @@ +# OctoCAT Supply Chain Management Application – General Copilot Instructions + +These are repository-wide guidelines. Path‑scoped files in `.github/instructions/*.instructions.md` provide focused guidance for specific areas (frontend, API, database). + +## High-Level Architecture +Multi-language monorepo under `src/` implementing the OctoCAT Supply Chain app. It ships FOUR interchangeable implementations of the same OctoCAT Supply API plus a shared UI and database: +- `src/api-ts` Express REST API in TypeScript (SQLite persistence, repository pattern, Swagger docs) +- `src/api-cs` ASP.NET REST API in C# +- `src/api-py` FastAPI REST API in Python +- `src/api-java` Spring Boot REST API in Java +- `src/frontend` React + Vite + Tailwind UI (shared across all API variants) +- `src/database` SQLite schema — `src/database/migrations/` and `src/database/seed/` +- `src/Makefile` orchestrates install/build/test/dev across the languages + +Refer to `src/docs/architecture.md` and `src/docs/sqlite-integration.md` for deeper details. Avoid restating them in reviews and link instead. + +## General Review Guidance +When generating suggestions: +1. Prefer incremental, minimal diffs; preserve existing style and naming. +2. Surface security, correctness, and data integrity issues before micro-optimizations. +3. Encourage type safety (no `any` unless justified). Suggest adding/refining model or DTO types when gaps appear. +4. Reduce duplication: when the same logic appears in multiple places, suggest extracting it into a shared utility or repository method. +5. Ensure error handling uses existing custom error types where appropriate (e.g., NotFound, Validation, Conflict) and propagates consistent HTTP status codes via middleware. +6. Encourage tests: request unit tests for new repository logic and component tests (or at least React Testing Library coverage) for critical UI paths. +7. For performance concerns, highlight N+1 query patterns, unnecessary data loading, or large bundle additions. +8. Prefer environment variable driven configuration; avoid hard‑coded paths/secrets. + +## Monorepo Workflow +- Build and test via the `src/Makefile` (run targets from `src/`): `make build` / `make test` / `make dev` cover all variants, and per-language targets like `make build-ts`, `make test-ts`, `make dev-ts` scope to one implementation. +- Install dependencies with the install targets: `make install` (everything) or per-language `make install-ts`, `make install-cs`, `make install-py`, `make install-java`, `make install-frontend`. +- Keep PRs scoped: code + tests + docs (architecture or build notes) when behavior changes. +- Update related instruction files if new folders or architectural slices are introduced. + +## Do Not Repeat +Do not inline full API route or component files in review feedback unless absolutely necessary: quote only the lines requiring change. Summarize low‑impact nits. + +## Escalation Order for Suggestions +1. Security / data integrity +2. Logical / functional correctness +3. Performance / scalability +4. Maintainability / duplication +5. Readability / consistency +6. Style / minor formatting + +## Tone & Feedback Style +Be concise, actionable, and cite a rationale ("because" clause) for non-trivial recommendations. Offer one preferred solution; optionally a lightweight alternative. + +--- +If new subsystems are added (e.g., `mobile/`, `worker/`), create a new `*.instructions.md` with `applyTo` globs instead of bloating this file. \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/api.instructions.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/api.instructions.md new file mode 100644 index 000000000..0947208eb --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/api.instructions.md @@ -0,0 +1,39 @@ +--- +description: "Guidance for editing and reviewing API code changes in the API." +applyTo: "src/api-ts/**, src/api-cs/**, src/api-py/**, src/api-java/**, src/database/migrations/**, src/database/seed/**" +--- +# API Review Guidance +Focus on correctness, security, data integrity, and consistency in the API and SQLite repository layer. The same OctoCAT Supply API is implemented in four languages — TypeScript/Express (`src/api-ts`), C#/ASP.NET (`src/api-cs`), Python/FastAPI (`src/api-py`), and Java/Spring (`src/api-java`) — so apply this guidance to whichever stack the change targets. + +## API Principles + +- Keep controllers (routes) thin: validation + orchestration; move logic to repositories/services. +- Use parameterized SQL always; never build raw query strings with user input. +- Return proper HTTP status codes via shared error classes (NotFound, Validation, Conflict). +- Preserve consistent naming: idiomatic casing in code models (e.g., camelCase in TypeScript) mapped reliably to snake_case SQL columns. +- Keep the API surface documented (Swagger/OpenAPI). Update the spec when adding/modifying endpoints. + +## Review Checklist +1. Input validation: basic type/shape checks before hitting DB; reject ambiguous/partial updates. +2. Error propagation: repository throws domain errors -> middleware -> correct status. +3. Transactions: group multi-table writes that must succeed or fail together. +4. Performance: watch for N+1 loops over rows triggering per-row SELECT; prefer JOIN queries. +5. Migrations: every schema change accompanied by new sequential SQL file; no edits to prior migrations. +6. Seed adjustments when adding required NOT NULL columns or reference data. +7. Swagger updated: new routes, models, examples, response codes. + +## Data Integrity +- Enforce foreign keys (ensure config keeps them ON) & add indexes for new FK columns. +- Use CHECK constraints or application validation for domain rules (e.g., quantity >= 0). + +## Testing Guidance +- Add unit tests for new repository methods (happy path + error cases) using in-memory DB. +- For route additions, integration test hitting the real app (e.g., supertest for Express, WebApplicationFactory for ASP.NET, TestClient for FastAPI, MockMvc for Spring) verifying status + response shape. + +## Security Considerations +- Sanitize / constrain pagination (max limits) to avoid table scans. +- Do not leak internal error stacks in production responses. +- CORS: restrict origins if production hardened later. + +## Example Feedback Style +"`ordersRepo.findWithDetails()` issues one SELECT per order detail (N+1). Consider a single JOIN query returning flattened rows then post-process." \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/database.instructions.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/database.instructions.md new file mode 100644 index 000000000..027063bee --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/database.instructions.md @@ -0,0 +1,38 @@ +--- +description: "Guidance for editing and reviewing database schema changes in the API." +applyTo: "src/database/migrations/**, src/database/seed/**, src/api-ts/src/db/**" +--- +# Database & Schema Review Guidance +Specialized guidance for SQLite schema evolution, migrations, and seed strategy. + +## Migration Principles +- Immutable historical migrations: never modify existing files—add a new sequential file. +- Each migration file must be idempotent or guarded (e.g., `IF NOT EXISTS`) where feasible. +- Provide matching DOWN/rollback strategy if environment supports manual reversal (comment rationale if impossible). + +## Schema Design Checklist +1. Foreign keys defined with ON DELETE / ON UPDATE behavior explicitly (avoid silent cascades unless intended). +2. Indexes on: foreign key columns, high-selectivity WHERE predicates, and composite indexes for frequent multi-column filters. +3. Use CHECK constraints for domain invariants (non-negative quantities, valid enum sets) instead of only application code. +4. Avoid over-normalization that complicates queries without clear benefit. +5. Prefer INTEGER primary keys (auto-increment) unless natural key is stable & required. + +## Performance & Optimization +- Evaluate query plans for slow endpoints (add covering indexes as needed). +- Consider partial indexes only if table sizes grow significantly; otherwise keep simple. + +## Seeding Guidelines +- Deterministic: ensure seeds produce same IDs each run (explicit INSERT IDs) when referenced. +- Keep seed data minimal but illustrative; large sample volumes belong in fixtures. +- Update seeds when adding NOT NULL columns without defaults; ensure referential order. + +## Review Red Flags +- Dropping and recreating tables for simple column additions (prefer ALTER when supported semantics suffice). +- Adding wide TEXT columns without need; consider normalization or constraints. +- Missing transaction around multi-statement data migrations altering existing rows. + +## Testing +- Use in-memory DB for unit tests; ensure migrations run before repository tests. + +## Example Feedback Style +"Migration 005 adds `order_status` but seeds not updated; existing seed inserts will violate NOT NULL. Add default or modify seed file." \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/frontend.instructions.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/frontend.instructions.md new file mode 100644 index 000000000..144d5d8a9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/instructions/frontend.instructions.md @@ -0,0 +1,38 @@ +--- +description: "Guidance for editing and reviewing frontend code changes in the React + Vite + Tailwind stack." +applyTo: "src/frontend/src/**, src/frontend/index.html, src/frontend/vite.config.ts, src/frontend/tailwind.config.js" +--- +# Frontend Review Guidance +Focus on UX quality, accessibility, performance, and maintainability of the React + Vite + Tailwind stack. + +## Key Principles +- Accessibility first: semantic HTML, proper labels, ARIA only when semantics insufficient, maintain focus order. +- State management: Prefer React Query for server cache; keep local UI state local; avoid prop drilling with context sparingly. +- Performance: Split lazy routes/components, avoid large bundle additions, prefer dynamic import for rarely used views. +- Styling: Tailwind utility classes preferred; abstract repeated class groups into small components or `clsx` helpers rather than custom CSS. +- Types: Avoid `any`; type API responses with shared DTO types; infer with generics where possible. + +## Review Checklist +1. Data fetching uses React Query (staleTime, error handling, loading states) not ad-hoc `useEffect` + `axios`. +2. Components remain small & focused (< ~150 LOC). Suggest extraction when crossing concerns (data + complex layout + formatting). +3. Responsive: verify critical views at mobile (≤640px), md (~768px), lg (≥1024px). +4. Form inputs: keyboard accessible, visible focus ring, validation feedback with text, not only color. +5. Images: optimized (correct size, `alt` text), avoid layout shift (width/height or aspect-ratio set). +6. Routing: use `react-router-dom` v7 patterns (data routers if adopted) and avoid nested suspense waterfalls. +7. Security: never interpolate untrusted HTML; sanitize if unavoidable. + +## Testing Guidance +- Encourage React Testing Library tests for complex logic (conditional rendering, form validation, custom hooks). +- Snapshot tests only for stable presentational components. + +## Performance Flags +- Re-render hotspots (large lists) should use `memo` or windowing (e.g., `react-window`) when count > ~200. +- Avoid anonymous inline functions in large list children where measurable. + +## Anti-Patterns to Nudge +- Overuse of context for simple prop passing. +- Mixing data fetching + presentational markup in one large component. +- Custom CSS files duplicating Tailwind utilities. + +## Example Feedback Style +"Consider extracting the price formatting into a `formatCurrency()` utility because it's duplicated in ProductCard and OrderSummary and risks divergence." \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/skills/frontend-design/SKILL.md b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/skills/frontend-design/SKILL.md new file mode 100644 index 000000000..decdff43d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/skills/frontend-design/SKILL.md @@ -0,0 +1,55 @@ +--- +name: frontend-design +description: Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults. +license: Complete terms in LICENSE.txt +--- + +# Frontend Design + +Approach this as the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's. This client has already rejected proposals that felt templated, and is paying for a distinctive point of view: make deliberate, opinionated choices about palette, typography, and layout that are specific to this brief, and take one real aesthetic risk you can justify. + +## Ground it in the subject + +If the brief does not pin down what the product or subject is, pin it yourself before designing: name one concrete subject, its audience, and the page's single job, and state your choice. If there's any information in your memory about the human's preferences, context about what they're building, or designs you've made before – use that as a hint. The subject's own world, its materials, instruments, artifacts, and vernacular, is where distinctive choices come from. Build with the brief's real content and subject matter throughout. + +## Design principles + +For web designs, the hero is a thesis. Open with the most characteristic thing in the subject's world, in whatever form makes sense for it: a headline, an image, an animation, a live demo, an interactive moment. Be deliberate with your choice: a big number with a small label, supporting stats, and a gradient accent is the template answer, only use if that's truly the best option. + +Typography carries the personality of the page. Pair the display and body faces deliberately, not the same families you would reach for on any other project, and set a clear type scale with intentional weights, widths, and spacing. Make the type treatment itself a memorable part of the design, not a neutral delivery vehicle for the content. + +Structure is information. Structural devices, numbering, eyebrows, dividers, labels, should encode something true about the content, not decorate it. Many generic designs use numbered markers (01 / 02 / 03), but that's only appropriate if the content actually is a sequence - like a real process or a typed timeline where order carries information the reader needs. Question if choices like numbered markers actually make sense before incorporating them. + +Leverage motion deliberately. Think about where and if animation can serve the subject: a page-load sequence, a scroll-triggered reveal, hover micro-interactions, ambient atmosphere. An orchestrated moment usually lands harder than scattered effects; choose what the direction calls for. However, sometimes less is more, and extra animation contributes to the feeling that the design is AI-generated. + +Match complexity to the vision. Maximalist directions need elaborate execution; minimal directions need precision in spacing, type, and detail. Elegance is executing the chosen vision well. + +Consider written content carefully. Often a design brief may not contain real content, and it's up to you to come up with copy. Copy can make a design feel as templated as the design itself. See the below section on writing for more guidance. + +## Process: brainstorm, explore, plan, critique, build, critique again + +For calibration: AI-generated design right now clusters around three looks: (1) a warm cream background (near #F4F1EA) with a high-contrast serif display and a terracotta accent; (2) a near-black background with a single bright acid-green or vermilion accent; (3) a broadsheet-style layout with hairline rules, zero border-radius, and dense newspaper-like columns. All three are legitimate for some briefs, but they are defaults rather than choices, and they appear regardless of subject. Where the brief pins down a visual direction, follow it exactly — the brief's own words always win, including when it asks for one of these looks. Where it leaves an axis free, don't spend that freedom on one of these defaults. Just like a human designer who's hired, there's often a careful balance between doing what you're good at and taking each project as a chance to experiment and learn. + +Work in two passes. First, brainstorm a short design plan based on the human's design brief: create a compact token system with color, type, layout, and signature. Color: describe the palette as 4–6 named hex values. Type: the typefaces for 2+ roles (a characterful display face that's used with restraint, a complementary body face, and a utility face for captions or data if needed). Layout: a layout concept, using one-sentence prose descriptions and ASCII wireframes to ideate and compare. Signature: the single unique element this page will be remembered by that embodies the brief in an appropriate way. + +Then review that plan against the brief before building: if any part of it reads like the generic default you would produce for any similar page (work through a similar prompt to see if you arrive somewhere similar) rather than a choice made for this specific brief — revise that part, say what you changed and why. Only after you've confirmed the relative uniqueness of your design plan should you start to write the code, following the revised plan exactly and deriving every color and type decision from it. + +When writing the code, be careful of structuring your CSS selector specificities. It's easy to generate CSS classes that cancel each other out (especially with a type-based selector like .section and a element-based selector like .cta). This can happen often with paddings/margins between sections. + +Try to do a lot of this planning and iteration in your thinking, and only show ideas to the user when you have higher confidence it'll delight them. + +## Restraint and self-critique + +Spend your boldness in one place. Let the signature element be the one memorable thing, keep everything around it quiet and disciplined, and cut any decoration that does not serve the brief. Not taking a risk can be a risk itself! Build to a quality floor without announcing it: responsive down to mobile, visible keyboard focus, reduced motion respected. Critique your own work as you build, taking screenshots if your environment supports it – a picture is worth 1000 tokens. Consider Chanel's advice: before leaving the house, take a look in the mirror and remove one accessory. Human creators have memory and always try to do something new, so if you have a space to quickly jot down notes about what you've tried, it can help you in future passes. + +## More on writing in design + +Words appear in a design for one reason: to make it easier to understand, and therefore easier to use. They are design material, not decoration. Bring the same intentionality to copy that you would bring to spacing and color. Before writing anything, ask what the design needs to say, and how it can best be said to help the person navigate the experience. + +Write from the end user's side of the screen. Name things by what people control and recognize, never by how the system is built. A person manages notifications, not webhook config. Describe what something does in plain terms rather than selling it. Being specific is always better than being clever. + +Use active voice as default. A control should say exactly what happens when it's used: "Save changes," not "Submit." An action keeps the same name through the whole flow, so the button that says "Publish" produces a toast that says "Published." The vocabulary of an interface is the signposting for someone navigating the product. Cohesion and consistency are how people learn their way around. + +Treat failure and emptiness as moments for direction, not mood. Explain what went wrong and how to fix it, in the interface's voice rather than a person's. Errors don't apologize, and they are never vague about what happened. An empty screen is an invitation to act. + +Keep the register conversational and tuned: plain verbs, sentence case, no filler, with tone matched to the brand and the audience. Let each element do exactly one job. A label labels, an example demonstrates, and nothing quietly does double duty. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.github/workflows/deploy.yml b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/workflows/deploy.yml new file mode 100644 index 000000000..025f7143f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.github/workflows/deploy.yml @@ -0,0 +1,122 @@ +# ============================================================================= +# deploy.yml — Challenge 5 starter workflow (OPTIONAL, SKELETON — not turnkey) +# ----------------------------------------------------------------------------- +# This is a TODO-marked scaffold, NOT a working deploy. It is inert by default: +# - the `push` trigger is commented out, so it never auto-runs, and +# - no secrets are configured, so `azure/login` would no-op until you set them. +# +# Your job (the interesting part): finish the # TODO steps with Copilot, wire up +# OIDC, and get a green, reachable deploy. See infra/README.md for the topology. +# +# ----- OIDC setup (no client secret is ever stored) -------------------------- +# 1. Create a user-assigned managed identity OR an app registration in Azure. +# 2. Grant it Contributor on the target resource group (or subscription scope). +# 3. Add a FEDERATED CREDENTIAL on it whose subject matches how this workflow +# runs, e.g. for the main branch: +# repo:/:ref:refs/heads/main +# (or `repo:/:environment:` if you gate on an environment). +# 4. Add these GitHub secrets — Settings > Secrets and variables > Actions: +# AZURE_CLIENT_ID (the identity/app client id) +# AZURE_TENANT_ID +# AZURE_SUBSCRIPTION_ID +# and these repo/environment VARIABLES: +# AZURE_RESOURCE_GROUP (e.g. octocat-hack-rg) +# AZURE_LOCATION (e.g. eastus) +# No AZURE_CREDENTIALS / client secret is used — auth is OIDC only. +# ============================================================================= + +name: Deploy to Azure (Challenge 5 — scaffold) + +on: + workflow_dispatch: + # TODO: Opt in to automatic deploys once the workflow is finished and you trust + # it. Kept commented out so this scaffold never runs destructively. + # push: + # branches: [ main ] + +# OIDC: `id-token: write` lets the job request a short-lived federated token that +# azure/login exchanges for Azure creds. `contents: read` is for checkout. +permissions: + id-token: write + contents: read + +env: + # Pulled from the GitHub secrets/variables described in the header comment. + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} + LOCATION: ${{ vars.AZURE_LOCATION }} + # TODO: Registry + image naming. Set the ACR name your Bicep produces (it is + # derived as acr) or read it from an output. + ACR_NAME: '' # TODO: e.g. octocatdevacr + IMAGE_TAG: ${{ github.sha }} + +jobs: + deploy: + name: Provision & deploy + runs-on: ubuntu-latest + # TODO: Consider binding to a GitHub Environment (e.g. `environment: dev`) so + # you can add reviewers/approval gates and environment-scoped secrets. + steps: + - name: Checkout + uses: actions/checkout@v4 + + # OIDC login — note there is NO `creds:` input and no client secret. + - name: Azure login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ env.AZURE_CLIENT_ID }} + tenant-id: ${{ env.AZURE_TENANT_ID }} + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} + + # TODO: Ensure the resource group exists (idempotent). Uncomment/complete. + - name: Create resource group + run: | + echo "TODO: create the resource group" + # az group create --name "$RESOURCE_GROUP" --location "$LOCATION" + + # TODO: Ensure the ACR exists before you can push to it. Options: + # (a) create it here with `az acr create` on first run, or + # (b) let the Bicep create it, then read ACR_NAME from a deployment output + # and split this into "provision infra" -> "build/push" -> "update apps". + # The two build contexts and their Dockerfiles are: + # - API: context src/api-ts Dockerfile src/api-ts/Dockerfile (port 3000) + # - Frontend: context src/frontend Dockerfile src/frontend/Dockerfile (port 80) + - name: Build & push images to ACR + run: | + echo "TODO: build and push both images" + # az acr login --name "$ACR_NAME" + # LOGIN_SERVER="$ACR_NAME.azurecr.io" + # docker build -t "$LOGIN_SERVER/api:$IMAGE_TAG" -f src/api-ts/Dockerfile src/api-ts + # docker build -t "$LOGIN_SERVER/frontend:$IMAGE_TAG" -f src/frontend/Dockerfile src/frontend + # docker push "$LOGIN_SERVER/api:$IMAGE_TAG" + # docker push "$LOGIN_SERVER/frontend:$IMAGE_TAG" + # Tip: `az acr build` can build server-side and avoids a local docker daemon. + + # TODO: Provision (or update) the infrastructure from the Bicep skeleton. + # Pass the image references you just pushed. Decide how you supply + # registry auth (admin creds vs. managed identity) — see infra/README.md. + - name: Deploy infrastructure (Bicep) + run: | + echo "TODO: az deployment group create against infra/main.bicep" + # az deployment group create \ + # --resource-group "$RESOURCE_GROUP" \ + # --template-file infra/main.bicep \ + # --parameters infra/main.parameters.json \ + # --parameters \ + # apiImage="$ACR_NAME.azurecr.io/api:$IMAGE_TAG" \ + # frontendImage="$ACR_NAME.azurecr.io/frontend:$IMAGE_TAG" + + # TODO: If you provision infra and build images in separate phases, roll the + # container apps to the new tags here (otherwise the deployment above + # already set them). Then print the frontend URL to verify. + - name: Update container apps to new image tags + run: | + echo "TODO: update the api + frontend container apps to :$IMAGE_TAG" + # az containerapp update -g "$RESOURCE_GROUP" -n \ + # --image "$ACR_NAME.azurecr.io/api:$IMAGE_TAG" + # az containerapp update -g "$RESOURCE_GROUP" -n \ + # --image "$ACR_NAME.azurecr.io/frontend:$IMAGE_TAG" + # az deployment group show -g "$RESOURCE_GROUP" -n main \ + # --query properties.outputs.frontendUrl.value -o tsv diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.gitignore b/03-Azure/01-04-AI/01_Agentic_SDLC/.gitignore new file mode 100644 index 000000000..1d9b66e24 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.gitignore @@ -0,0 +1,68 @@ +# .gitignore for the Agentic SDLC MicroHack +# +# Scoped to 03-Azure/01-04-AI/01_Agentic_SDLC/. Keeps build artifacts and local +# runtime files out of git when participants build/run the bundled Octocat Supply +# app (src/), the infra scaffold (infra/), or the helper scripts (scripts/). + +# OS / editor cruft +.DS_Store +Thumbs.db +.idea/ +.vs/ + +# Playwright local runtime / browser cache (Makefile test targets) +.playwright-mcp/ +src/.cache/ms-playwright/ + +# TypeScript / Node artifacts +node_modules/ +dist/ +coverage/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# .NET build artifacts +**/[Bb]in/ +**/[Oo]bj/ +*.user +*.rsuser +*.suo +*.userprefs +*.nupkg +*.snupkg +TestResults/ + +# Python artifacts +**/__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ +.venv/ +src/api-py/uploads/ + +# Java artifacts +**/target/ +*.class +*.jar +*.war +*.ear + +# Local database files (SQLite runtime) +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +# General logs +*.log diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/launch.json b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/launch.json new file mode 100644 index 000000000..e9c94ae40 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/launch.json @@ -0,0 +1,41 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug API", + "cwd": "${workspaceFolder}/api", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "console": "integratedTerminal", + }, + { + "type": "node", + "request": "launch", + "name": "Start Frontend", + "cwd": "${workspaceFolder}/frontend", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "console": "integratedTerminal", + "env": { + "VITE_API_URL": "http://localhost:3000" + }, + "serverReadyAction": { + "pattern": "Local:\\s+http://localhost:([0-9]+)", + "uriFormat": "http://localhost:%s", + "action": "openExternally" + } + } + ], + "compounds": [ + { + "name": "Start API & Frontend", + "configurations": ["Debug API", "Start Frontend"], + "stopAll": true, + "presentation": { + "order": 1 + } + } + ] +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/mcp.json b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/mcp.json new file mode 100644 index 000000000..a889cd4a9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/mcp.json @@ -0,0 +1,37 @@ +{ + "inputs": [ + { + "id": "github_token", + "type": "promptString", + "description": "GitHub Personal Access Token", + "password": true + } + ], + "servers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--browser=msedge" + ], + "type": "stdio" + }, + "github-remote": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "X-MCP-Toolsets": "actions, code_security, dependabot, discussions, issues, orgs, projects, pull_requests, repos, secret_protection, security_advisories, copilot, copilot_spaces" + } + }, + "Azure MCP Server": { + "command": "npx", + "args": [ + "-y", + "@azure/mcp@latest", + "server", + "start" + ], + "type": "stdio" + } + } +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/tasks.json b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/tasks.json new file mode 100644 index 000000000..27c7c9319 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/.vscode/tasks.json @@ -0,0 +1,59 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build All", + "type": "shell", + "dependsOrder": "sequence", + "command": "make", + "args": [ + "build" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Build API", + "type": "shell", + "command": "make", + "args": [ + "build-api" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [ + "$tsc" + ] + }, + { + "label": "Build Frontend", + "type": "shell", + "command": "make", + "args": [ + "build-frontend" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [ + "$tsc" + ] + }, + ], + "inputs": [ + ] +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/Readme.md b/03-Azure/01-04-AI/01_Agentic_SDLC/Readme.md new file mode 100644 index 000000000..f5106fa29 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/Readme.md @@ -0,0 +1,121 @@ +# **MicroHack: Agentic SDLC with GitHub Copilot & Azure** + +- [**MicroHack introduction**](#microhack-introduction) +- [**MicroHack context**](#microhack-context) +- [**Objectives**](#objectives) +- [**MicroHack challenges**](#microhack-challenges) +- [**Contributors**](#contributors) + +## MicroHack introduction + +This MicroHack walks a team through the **Agentic Software Development Lifecycle (SDLC)** — plan, implement, test, review, deploy, and operate with agents — using **GitHub Copilot**, **GitHub Actions**, and **Microsoft Azure**, applied end-to-end to a real full-stack application called **Octocat Supply**. + +Rather than treating an AI coding assistant as autocomplete, you practise driving *agents* across the whole lifecycle: refining requirements from organizational signal, planning multi-file changes, generating and hardening tests, reviewing code, deploying to Azure with Infrastructure as Code, monitoring with the Azure SRE Agent, and standing up agentic workflows that maintain the app over time. + +```mermaid +flowchart LR + A[Plan] --> B[Implement] + B --> C[Test] + C --> D[Review] + D --> E[Deploy to Azure] + E --> F[Operate / Monitor] + F --> G[Maintain] + G -.-> A + subgraph Agents + Copilot[GitHub Copilot / Copilot App] + Actions[GitHub Actions CI/CD] + SRE[Azure SRE Agent] + GHAW[GitHub Agentic Workflows] + end + Copilot --- A + Copilot --- B + Copilot --- C + Copilot --- D + Actions --- E + SRE --- F + GHAW --- G +``` + +This lab is **not** a full explanation of GitHub Copilot or Azure as technologies. The following are recommended pre-reading to build foundational knowledge: + +- [GitHub Copilot documentation](https://docs.github.com/en/copilot) +- [GitHub Actions documentation](https://docs.github.com/en/actions) +- [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/) +- [Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/) + +> **What's included.** This MicroHack folder is **self-contained** — everything you need ships with it: the **Octocat Supply** application source ([`src/`](src/README.md)), the Azure infrastructure scaffold ([`infra/`](infra/README.md)), helper [`scripts/`](scripts/README.md), the dev containers ([`.devcontainer/`](.devcontainer/)), the editor config ([`.vscode/`](.vscode/)), and the repo custom instructions, custom agents, and workflows ([`.github/`](.github/)). The Challenge 3 inputs — the product backlog and the mock **WorkIQ** organizational knowledge (Teams thread, support-ticket digest, stakeholder email) — live under [`assets/`](assets/). Facilitators create a copy/fork of this MicroHack per team before the event; participants work in their team's copy. + +## MicroHack context + +The Octocat team are behind schedule launching their new **Octocat Supply** application and are exploring Agentic Development practices to help them deliver the remaining features before deploying the full application to their Azure environment. Core functionality is still missing — like the shopping cart and other backlog items — and the testing and monitoring aren't yet up to Octocat standards for a reliable, secure application. + +You are given the existing application code and access to GitHub tools such as GitHub Copilot, and you complete the work by progressing through the challenges. The app is a full-stack supply chain platform with a **React + Vite + Tailwind** frontend and **four interchangeable API implementations** (TypeScript, C#, Python, Java) over a shared **SQLite** schema — pick whichever API language your team prefers; the challenges are language-agnostic. + +## Objectives + +After completing this MicroHack you will be able to: + +- Use GitHub Copilot to **plan and implement** features across a full-stack app while keeping changes scoped and consistent. +- Run an **agentic loop** — plan → implement → test → review — on a backlog item, including refining a requirement from ambient organizational signal. +- **Generate and extend tests** deliberately, choosing models and context budgets on purpose, and run an agent-assisted code review. +- **Deploy to Azure** with Infrastructure as Code and a GitHub Actions pipeline authenticated via OIDC — no secrets in source. +- Use the **Azure SRE Agent** to detect, diagnose, and remediate a fault in a deployed app. +- Stand up **GitHub Agentic Workflows** to maintain the application over time with clear triggers and guardrails. + +## MicroHack challenges + +### General prerequisites + +This MicroHack has a few but important prerequisites. Completing them before the session keeps the time focused on the differentiated, agentic work rather than setup. + +In summary: + +- A **GitHub account** with access to your team's copy of this MicroHack repo and an **active GitHub Copilot seat** (Copilot Business/Enterprise, with Chat, agents, CLI, and MCP policies enabled). +- Either **GitHub Codespaces** (recommended — nothing to install) **or** local **VS Code** with the **Dev Container** plus the **GitHub Copilot / Copilot Chat** extensions. The container preinstalls **Node.js 24, Python 3.13, .NET 10, Java 17**. +- For **Challenges 5 & 6**, **Azure access**: + - An **Azure subscription** with budget. + - A **Resource Group** for the team. + - **Contributor** permissions on that scope (plus any roles the Azure SRE Agent needs on the monitored resources). + - A GitHub ↔ Azure **OIDC federated credential** for pipeline authentication. + - **Azure SRE Agent** available in the team's region/subscription. + +> Facilitators: work through the pre-event admin checklist (Copilot enablement, repo access, backlog issue seeding, Azure prerequisites) before running the event. + +### Challenges + +* [Challenge 1 - Dev environment, Copilot access & repo overview](challenges/challenge-01.md) **<- Start here** +* [Challenge 2 - Deliver a new feature (the cart)](challenges/challenge-02.md) +* [Challenge 3 - Add another feature from the backlog (agentic loop)](challenges/challenge-03.md) +* [Challenge 4 - Run and extend test coverage, and review code](challenges/challenge-04.md) +* [Challenge 5 - Deploy into Azure](challenges/challenge-05.md) +* [Challenge 6 - Monitor the deployed app with the Azure SRE Agent](challenges/challenge-06.md) +* [Challenge 7 - Implement agentic workflows to maintain the app](challenges/challenge-07.md) +* [Finish](challenges/finish.md) + +### Solutions - Spoilerwarning + +Do not open these before attempting the challenges — they contain guidance and worked approaches. + +* [Solution 1 - Dev environment & repo overview](walkthrough/challenge-01/solution-01.md) +* [Solution 2 - Deliver the cart feature](walkthrough/challenge-02/solution-02.md) +* [Solution 3 - Backlog feature (agentic loop)](walkthrough/challenge-03/solution-03.md) +* [Solution 4 - Testing & review](walkthrough/challenge-04/solution-04.md) +* [Solution 5 - Deploy into Azure](walkthrough/challenge-05/solution-05.md) +* [Solution 6 - Monitor with the Azure SRE Agent](walkthrough/challenge-06/solution-06.md) +* [Solution 7 - Agentic workflows](walkthrough/challenge-07/solution-07.md) + +## Suggested agenda + +A typical one-day delivery of this MicroHack: + +| Time | Activity | +| --- | --- | +| 09:00 – 10:00 | Tech Talk — Agentic SDLC overview | +| 10:00 – 12:30 | Hacking session — Challenges 1, 2, 3, 4 | +| 12:30 – 13:30 | Lunch break | +| 13:30 – 15:30 | Hacking session — Challenges 5, 6, 7 | +| 15:30 – 16:00 | Final discussion / Wrap-up | + +## Contributors + +* Francesco Lana ([GitHub](https://github.com/Frank802), [Linkedin](https://www.linkedin.com/in/franklana)) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/assets/backlog.md b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/backlog.md new file mode 100644 index 000000000..6935ad78f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/backlog.md @@ -0,0 +1,184 @@ +# Octocat Supply — Product Backlog + +This is the **Challenge 3 backlog**: a curated, prioritised list of features the Octocat team still +needs to deliver for the Octocat Supply application. Instead of inventing a feature, pick one of the +items below and take it through a full agentic loop — **plan → implement → test → review**. + +Every item is **language-agnostic**. It can be delivered against any API variant (TypeScript, Python, +.NET, or Java) plus the shared React + Vite + Tailwind frontend, all backed by the same SQLite schema +(suppliers, headquarters, branches, products, orders, order details, deliveries, inventory). Descriptions +and acceptance criteria describe behaviour, not a specific stack — implement them using your track's +existing patterns (repository pattern, DTOs/types, error handling, consistent HTTP status codes). + +A set of these items may already be seeded as **GitHub Issues** in your team repo (see +[`scripts/seed-backlog.sh` / `scripts/seed-backlog.ps1`](../scripts/README.md)). If so, you can pick from +real, labelled issues — and the same issues are what an MCP server can pull as real backlog context in +the Challenge 3 optional stretch. + +## How to use this backlog + +1. Pick **one** item that your team can finish *with tests* inside the 60-minute challenge — the size + (S/M/L) is there to help you scope. +2. Use the acceptance criteria as your definition of done. +3. Keep the diff scoped and consistent with the existing codebase; add tests for the new logic. + +> **Recommended starting point:** _Payment integration for the cart_ — it's the natural continuation of +> the cart feature from Challenge 2. + +## Priority & sizing legend + +- **Priority** — High / Medium / Low (business value + launch-readiness) +- **Size** — S (small, ~single layer), M (medium, spans API + frontend), L (large, spans the full stack) + +--- + +## 1. Payment integration for the cart ⭐ (recommended) + +- **Priority:** High +- **Size:** M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:high` + +> As a customer, I want to pay for the items in my cart so that I can complete a purchase and turn my +> cart into a confirmed order. This is the natural continuation of the cart feature from Challenge 2. + +**Acceptance criteria** + +- [ ] A customer can start a checkout from the cart and submit payment details through a dedicated flow. +- [ ] A successful payment converts the cart into an order (with order details) via the API. +- [ ] Payment configuration (keys/provider) is read from environment variables — no secrets in source. +- [ ] Failed or declined payments surface a clear error and leave the cart intact. + +--- + +## 2. Order history + +- **Priority:** Medium +- **Size:** M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:medium` + +> As a customer, I want to see a list of my past orders and open any one of them so that I can review +> what I bought and when. + +**Acceptance criteria** + +- [ ] The API exposes an endpoint that returns orders (with their order details) for a customer/branch. +- [ ] The frontend shows an order-history list with date, status, and total per order. +- [ ] Selecting an order shows its line items (product, quantity, unit price). +- [ ] Empty state is handled gracefully when there are no orders yet. + +--- + +## 3. Product search & filtering + +- **Priority:** Medium +- **Size:** M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:medium` + +> As a customer, I want to search and filter the product catalogue so that I can quickly find the +> products I need instead of scrolling the whole list. + +**Acceptance criteria** + +- [ ] The products endpoint accepts query parameters for a text search (name/SKU) and at least one filter + (e.g. supplier or price range). +- [ ] The products page has a search box and filter control wired to the API. +- [ ] Results update to reflect the active search/filter, and clearing them restores the full list. +- [ ] Search is case-insensitive and handles "no results" cleanly. + +--- + +## 4. Inventory management / stock levels + +- **Priority:** High +- **Size:** L +- **Affected layers:** frontend, API, database +- **Suggested label:** `backlog`, `priority:high` + +> As a branch manager, I want each product to track a stock level so that customers can't order more than +> is available and staff can see what needs restocking. + +**Acceptance criteria** + +- [ ] Products carry a stock/quantity-on-hand value (schema/migration added following existing patterns). +- [ ] The API returns stock levels and prevents ordering more than the available quantity. +- [ ] The frontend shows stock status (e.g. in stock / low / out of stock) on product views. +- [ ] Placing an order decrements the relevant stock, and the change is covered by tests. + +--- + +## 5. Supplier & branch management view + +- **Priority:** Medium +- **Size:** M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:medium` + +> As an admin, I want to view and manage suppliers and branches so that I can keep the organisation's +> supply-chain data accurate, mirroring the existing product admin experience. + +**Acceptance criteria** + +- [ ] An admin view lists suppliers (and/or branches) using the existing admin UI patterns. +- [ ] An admin can create or edit a supplier/branch through the API. +- [ ] Basic validation is applied (e.g. required name) with consistent error responses. +- [ ] The view is reachable from the existing admin navigation. + +--- + +## 6. Delivery tracking & status + +- **Priority:** Medium +- **Size:** M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:medium` + +> As a branch manager, I want to see deliveries and their status so that I can track incoming stock from +> suppliers against the orders that need it. + +**Acceptance criteria** + +- [ ] The API exposes deliveries with their status and links to the related order details. +- [ ] The frontend shows a deliveries list with supplier, date, and status. +- [ ] A delivery's status can be advanced (e.g. pending → shipped → delivered) via the API. +- [ ] Status changes are validated and reflected in the UI. + +--- + +## 7. Order status workflow + +- **Priority:** Medium +- **Size:** S–M +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:medium` + +> As a branch manager, I want to move an order through a clear set of statuses so that everyone can see +> where each order stands from placement to fulfilment. + +**Acceptance criteria** + +- [ ] Orders support a defined set of statuses (e.g. pending → confirmed → shipped → delivered / cancelled). +- [ ] The API only allows valid status transitions and rejects invalid ones with a clear error. +- [ ] The frontend shows the current status and allows an authorised user to advance it. +- [ ] The transition rules are covered by unit tests. + +--- + +## 8. Cart quantity validation (quality-of-life) + +- **Priority:** Low +- **Size:** S +- **Affected layers:** frontend, API +- **Suggested label:** `backlog`, `priority:low` + +> As a customer, I want the cart to reject nonsensical quantities so that I can't accidentally order zero, +> a negative amount, or a non-numeric quantity. + +**Acceptance criteria** + +- [ ] Quantities must be positive integers; invalid values are rejected on both the API and the UI. +- [ ] The API returns a consistent validation error for bad quantities. +- [ ] The cart UI shows an inline message and blocks the update/checkout until it's fixed. +- [ ] The validation logic has unit-test coverage. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/README.md new file mode 100644 index 000000000..06dc2f8f4 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/README.md @@ -0,0 +1,22 @@ +# WorkIQ — mock organizational knowledge + +**WorkIQ** is this hackathon's stand-in for **Microsoft 365 organization knowledge, context & insights** — the kind of signal an agent could pull from Teams chats, support systems, and stakeholder email. None of it is real; it's a small, deliberately **messy** slice of "what the org is talking about" so you can practise turning ambient organizational chatter into implementable work. + +This folder exists for the **Challenge 3 optional stretch** (requirement refinement). It gives you something *raw* to refine — there is no clean story, no spec, and no acceptance criteria for you to copy. That's the point. + +## What's in here + +| File | What it mocks | +| --- | --- | +| [`teams-thread-launch-promo.md`](teams-thread-launch-promo.md) | A Microsoft Teams thread across a few people ahead of the Octocat Supply launch | +| [`support-tickets-digest.md`](support-tickets-digest.md) | A digest of raw customer support tickets (customer voice, unedited) | +| [`stakeholder-email-launch-priorities.md`](stakeholder-email-launch-priorities.md) | A short launch-week priorities email from a stakeholder | + +## How to use it + +1. **Point your agent at this folder** as organizational context (open the files, or reference `assets/workiq/` in your prompt). +2. **Synthesize across the artifacts** — read the chat, the tickets, and the email together and look for a need that shows up in *more than one* place. +3. **Separate signal from noise.** Not everything here is new or actionable. Some of it restates work that's **already on the backlog** ([`assets/backlog.md`](../backlog.md)); some of it is just people being people. Your job is to find the genuine, *unmet* need that isn't already planned. +4. **Refine it into issue-ready work** — turn that need into a crisp requirement: a **problem statement**, **testable acceptance criteria**, and a small set of **scoped tasks** that fit the challenge timebox. Optionally open it as a GitHub Issue. + +> There is **no answer key**. A good refinement is judged on the *quality of the refinement* — a clear problem, testable criteria, sensible scope — not on guessing a "correct" feature. Keep it small enough to actually deliver. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/stakeholder-email-launch-priorities.md b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/stakeholder-email-launch-priorities.md new file mode 100644 index 000000000..1be3eaeef --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/stakeholder-email-launch-priorities.md @@ -0,0 +1,35 @@ +# Email — launch-week priorities + +> Mock stakeholder email. Fictional, for the hackathon. + +--- + +**From:** Elena Vasquez +**To:** Octocat Supply Delivery Team +**Cc:** Priya Nair; Marcus Bell +**Date:** Mon, 9 Jun 2025, 08:32 +**Subject:** Launch week — what I need us focused on + +Team, + +We're in the final stretch and I want everyone pointed at the same handful of things. Not an exhaustive list — the "if these slip, launch feels broken" list. + +In rough priority order: + +1. **Checkout has to let people actually buy.** Getting from a full cart to a paid, confirmed order is the whole ballgame. This is the top of the list — everything else is behind it. + +2. **Marketing is counting on discount codes for launch week.** The entire campaign is built around an offer, and we've told customers — including at least one big branch account Dan closed — that they'll be able to use a code when they buy. Right now the product has no way for a customer to do that, and support is already getting asked. I don't want launch day to be the moment we discover we can't honour what marketing promised. Someone needs to take this from "everyone agrees we need it" to a properly shaped, testable piece of work — please pull the context together (the launch channel and the support queue both have plenty) rather than guessing at the details. + +3. **Customers keep asking to see their past orders.** I know this is already planned — I'm not adding it, just reinforcing it matters for the repeat-order accounts. + +4. **Product search** would take a lot of friction out of large orders. Also, I believe, already on the backlog. Lower than the above for launch but keep it warm. + +Let's not gold-plate any of these. I'd rather have the top two genuinely solid than four things half-done. Priya's going to help shape the newer asks into something the team can actually pick up — if it's not clearly scoped with a way to test it, it's not ready. + +Thanks — this is going to be a good one. + +Elena + +— +Elena Vasquez +VP Product, Octocat Supply diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/support-tickets-digest.md b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/support-tickets-digest.md new file mode 100644 index 000000000..2b300deeb --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/support-tickets-digest.md @@ -0,0 +1,45 @@ +# Support tickets — weekly digest + +> Mock export from the customer support queue. Raw customer wording kept as-is (typos and all). Fictional, for the hackathon. + +**Queue:** Octocat Supply — Pre-launch +**Window:** last 7 days · 7 tickets + +--- + +**#4821** · 2025-06-02 · *Discount code has nowhere to go* +Got the email about the launch offer and it says enter my code at checkout but when I go through my cart theres no box to type it in? Where am I supposed to put it. Code is LAUNCH-ish something, happy to send it. + +--- + +**#4822** · 2025-06-02 · *Can't actually pay* +I've added stuff to my cart but I can't see how to actually pay for any of it. Is checkout not switched on yet? Need to place this order for my branch this week. + +--- + +**#4826** · 2025-06-03 · *"invalid" code??* +A colleague forwarded me a promo code and when I tried to use it the site just did nothing. No error, no discount, nothing. Is the code dead or is the feature not live. Would be nice to at least be told if a code is wrong. + +--- + +**#4830** · 2025-06-04 · *Where's my past orders* +How do I look at orders I placed last month? I want to reorder the same items but I cant find a history anywhere. Currently writing them down on paper which is silly. + +--- + +**#4833** · 2025-06-04 · *promo from newsletter not working* +Signed up to the newsletter, got a code as promised, went to check out and there is literaly no field for it. Am I missing a step? Feels half finished tbh. + +--- + +**#4838** · 2025-06-05 · *Search?* +Is there a way to search products by name? Scrolling the whole catalogue to find one item is painful when you know exactly what you want. + +--- + +**#4841** · 2025-06-06 · *Expired code still "works" / doesn't* +Tried an old code from a flyer, wasn't sure if it had run out. Nothing on screen told me either way — it just charged full price. If codes expire can you say so instead of silently ignoring it? + +--- + +*Triage note (support lead):* Cluster of tickets around entering / validating a **discount code** at checkout — currently no such capability exists in the product. Separately: the usual "can't pay" and "past orders" and "search" asks, which I believe are already on the roadmap — leaving those for the backlog rather than raising fresh. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/teams-thread-launch-promo.md b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/teams-thread-launch-promo.md new file mode 100644 index 000000000..9c6cab0be --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/assets/workiq/teams-thread-launch-promo.md @@ -0,0 +1,62 @@ +# Teams — "Octocat Supply launch" channel + +> Mock export of a Microsoft Teams channel thread. Names and content are fictional, for the hackathon. + +**Channel:** Octocat Supply Launch 🚀 +**Thread:** Launch week — final bits + +--- + +**Priya Nair** (PM) · 09:14 +Morning all — 11 days to launch 😅 Pulling together the final "what absolutely has to be in" list. If it's on your mind and it's not written down somewhere, drop it here so it doesn't get lost. + +**Marcus Bell** (Marketing) · 09:18 +Big one from our side: we're running a **launch campaign** and the whole thing hangs off a discount offer. Newsletter's already drafted, socials are teed up. People will be arriving with an offer expecting to use it. + +**Marcus Bell** (Marketing) · 09:19 +We've been saying "enter your code at checkout" in the creative. So… that needs to be a thing people can actually do 🙈 + +**Priya Nair** (PM) · 09:21 +Noted. Marcus can you make sure the exact wording of the offer lands with us and not just in the newsletter tool? We keep finding out about promises after the fact. + +**Marcus Bell** (Marketing) · 09:22 +Fair. Will do. + +**Dan Okonkwo** (Sales) · 09:26 +Related — I've got a large branch account (Ferndale, big first order) that was **promised a code** during the deal. They're expecting it to work on day one. I don't want to be the one telling them it doesn't 😬 + +**Dan Okonkwo** (Sales) · 09:27 +Also they asked (again) about seeing their **past orders** in one place. I know that's already on the backlog, just flagging the customer keeps raising it. + +**Priya Nair** (PM) · 09:29 +Order history is tracked already, yep — separate thread. Let's keep this one on launch-blockers. + +**Sofia Ramos** (Support) · 09:41 +From the support side: we're *already* getting "**where do I enter my discount code?**" tickets and we haven't even launched the campaign 🤔 people saw the teaser and went looking. Right now there's just… nowhere to put it. The cart goes straight to totals. + +**Sofia Ramos** (Support) · 09:42 +Handful of the usual "how do I pay" ones too but that's the payment work you already know about. + +**Priya Nair** (PM) · 09:44 +Ok so the theme I'm hearing: customers will show up with a code, and today the checkout has no concept of one at all. Marketing needs it, sales promised it, support is already fielding it. + +**Ivan Petrov** (Dev) · 09:52 +Reading back — from the app's side there's genuinely no notion of a code today. Cart adds up line items and that's the total, full stop. Whatever we decide, the total has to be able to change based on something the customer types in, and we'd need to reject the ones that aren't valid / have expired so people can't just make them up. + +**Ivan Petrov** (Dev) · 09:53 +Not saying how to build it here, just flagging it's net-new behaviour, not a tweak to something existing. + +**Dan Okonkwo** (Sales) · 09:55 +And please can invalid ones fail *clearly*. Last thing I need is Ferndale typing their code, nothing happening, and them assuming we lied to them. + +**Priya Nair** (PM) · 09:58 +Good input all. I'll write the launch-priorities note up properly and send it round so this isn't just buried in chat. Someone on the hack team can take the raw need and shape it into something we can actually put on the board. + +**Marcus Bell** (Marketing) · 10:01 +🙌 + +**Sofia Ramos** (Support) · 10:03 +One more unrelated thing while I have you — a few customers keep asking where their **delivery** is once an order's placed. Not launch-blocking, I think it's already planned, just noting the volume. + +**Priya Nair** (PM) · 10:04 +Yep, delivery tracking's on the backlog already. Parking it. Thanks all 🙏 diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md new file mode 100644 index 000000000..58da89218 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md @@ -0,0 +1,39 @@ +# Challenge 1 - Dev environment, Copilot access & repo overview + +**[Home](../Readme.md)** - [Next Challenge](challenge-02.md) + +## Goal + +Configure your development environment, confirm GitHub Copilot access, and get familiar with the **Octocat Supply** application so the later challenges run without setup interruptions. + +The Octocat team is behind schedule and adopting Agentic Development practices. Before you can help deliver features, you need a working environment, Copilot enabled, and a clear picture of the codebase. + +## Actions + +* **Get the code** — Open your team's copy of this MicroHack in **GitHub Codespaces** (recommended), a local **VS Code Dev Container** (`.devcontainer/`), or locally. Confirm you're on the intended working branch. +* **Verify runtimes** — Confirm the runtimes for your chosen API track are available (TypeScript, Python, .NET, or Java) plus Node.js for the frontend. In Codespaces / the Dev Container these are preinstalled. +* **Enable GitHub Copilot** — Sign in and confirm your Copilot entitlement is active in your IDE; install the GitHub Copilot / Copilot Chat extensions. +* **Explore the repo** — Review the structure: `src/` (the app, with `src/docs/` architecture guidance), `infra/` + `scripts/` (Azure deployment), `assets/` (Challenge 3 backlog & WorkIQ inputs), and `.github/` (Copilot custom instructions & agents). Identify where the frontend, APIs, and database live. Read `src/README.md` for the code structure and build commands. +* **Run the app locally** — Start the frontend and one API to confirm the application builds and runs before you begin changing it. + +## Success criteria + +* Your team's repo copy is open in your environment and Copilot is active and responding in your IDE. +* You can build and run the frontend and at least one API locally. +* You can describe, at a high level, how the frontend, APIs, and database fit together. + +### Optional stretch — repo-wide agent context + +Give Copilot persistent, repo-wide context so later challenges start with your conventions loaded. + +* Create an `AGENTS.md` and/or `.github/copilot-instructions.md` recording coding conventions, naming, and preferred patterns. +* Record the exact **build and test commands** for your API track (and the frontend) so an agent can verify its own work. +* Add explicit **scope rules** (e.g. don't touch generated files, infra, unrelated services) to keep agent diffs minimal. +* In a fresh Copilot Chat, ask a question that relies on those conventions **without restating them** and confirm they're applied. + +## Learning resources + +* [Setting up GitHub Copilot](https://docs.github.com/en/copilot/quickstart) +* [GitHub Codespaces](https://docs.github.com/en/codespaces/overview) +* [Development Containers](https://containers.dev/) +* [Adding repository custom instructions for Copilot](https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-02.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-02.md new file mode 100644 index 000000000..a6446627e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-02.md @@ -0,0 +1,36 @@ +# Challenge 2 - Deliver a new feature (the cart) + +**[Home](../Readme.md)** - [Previous Challenge](challenge-01.md) - [Next Challenge](challenge-03.md) + +## Goal + +Deliver a new feature for the Octocat Supply application by completing the incomplete shopping **cart** functionality, using GitHub Copilot to move fast while keeping the code consistent with the existing app. + +The shopping cart is incomplete and lacks payment integration, which is blocking the launch. The focus of this challenge is **using Copilot well** — giving it the right context and iterating — not just writing code. + +## Actions + +* **Understand the current state** — Review the existing cart-related code in the frontend and API. Use Copilot to explain what exists and what's missing. +* **Design the feature** — Decide the cart behaviours to implement: add item, update quantity, remove item, view cart total. +* **Implement the API** — Add or complete the cart endpoints following the repository's patterns (repository pattern, DTOs/types, error handling, consistent HTTP status codes). +* **Implement the UI** — Wire up the frontend cart view to the API using the React + Vite + Tailwind stack. +* **Validate** — Manually exercise the cart flow end to end in the running app. + +## Success criteria + +* A user can add products to the cart, update quantities, and remove items. +* The cart displays an accurate total. +* Cart state is persisted through the API using existing patterns (not just client-side React state). +* Changes are type-safe and consistent with the existing codebase style, with a scoped diff. + +### Optional stretch — structured prompting & context retrieval + +* Build a **reusable prompt template** with clear sections: objective, constraints, files in scope, desired output format. +* Set a **context-retrieval order**: directly edited files first → adjacent tests → architecture docs only if still needed. Stop once it's enough. +* Run the cart prompt structured, then run the same request with a loose, unstructured prompt and keep the better result — the comparison is the learning. + +## Learning resources + +* [Prompt engineering for GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/prompt-engineering-for-github-copilot) +* [Best practices for using GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/best-practices-for-using-github-copilot) +* [Getting started with Copilot Chat](https://docs.github.com/en/copilot/github-copilot-chat/using-github-copilot-chat) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-03.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-03.md new file mode 100644 index 000000000..ae3e8ed41 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-03.md @@ -0,0 +1,34 @@ +# Challenge 3 - Add another feature from the backlog (agentic loop) + +**[Home](../Readme.md)** - [Previous Challenge](challenge-02.md) - [Next Challenge](challenge-04.md) + +## Goal + +Pick up an additional feature from the Octocat Supply backlog and deliver it with a fuller **agentic loop** — plan → implement → test → review. With the cart underway, the team needs to keep burning down the backlog before launch. + +## Actions + +* **Choose a backlog item** — Pick a meaningful feature from `assets/backlog.md` (e.g. payment integration for the cart, order history, product search/filtering, or inventory management). A set of these may already be seeded as **GitHub Issues** in your repo — check the Issues tab and pick one. +* **Plan with an agent** — Use GitHub Copilot to break the work into steps and identify the files and layers involved (frontend, API, database) **before** writing code. +* **Implement** — Build the feature across the stack, keeping changes consistent with existing patterns and type-safe. +* **Add tests** — Cover the new logic with unit tests (API/repository) and component tests where relevant. +* **Review** — Use Copilot to review your own change before considering it done. + +## Success criteria + +* A new, working backlog feature is delivered end to end and demonstrable in the running app. +* The feature has appropriate automated test coverage. +* Code follows existing repository conventions and is type-safe. +* You used an agentic plan → implement → test → review loop and can point to the agent's plan, the implementation, the tests, and the review pass. + +### Optional stretch — harnesses, MCP & requirement refinement + +* **Compare harnesses** — Run the planning step twice: once in **Copilot CLI** and once in the **Copilot App / IDE chat**. Note which gave the more implementation-ready plan for a quick fix vs. a multi-file change, and write down a decision rule. +* **Wire in an MCP server** — Connect one MCP server (e.g. the **GitHub MCP server**) to pull real backlog context such as issues and PRs. Note what stays local-only vs. what the server can see. +* **Refine a requirement from organizational signal** — Point your agent at `assets/workiq/` — the mock **WorkIQ** organizational knowledge (Teams thread, support-ticket digest, stakeholder email). Have it synthesize the signal across those artifacts, separate a genuinely-new need from noise and from work **already in the backlog**, and shape one unmet need into an **issue-ready requirement**: problem statement → acceptance criteria → scoped tasks (optionally opening it as a GitHub Issue). There's no answer key — the skill is the refinement. A ready-made **Requirement Refiner** custom agent (`.github/agents/`) can guide this. + +## Learning resources + +* [About GitHub Copilot coding agent](https://docs.github.com/en/copilot/using-github-copilot/coding-agent) +* [Extending Copilot Chat with the Model Context Protocol (MCP)](https://docs.github.com/en/copilot/customizing-copilot/extending-copilot-chat-with-mcp) +* [GitHub MCP server](https://github.com/github/github-mcp-server) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-04.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-04.md new file mode 100644 index 000000000..09decdb23 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-04.md @@ -0,0 +1,35 @@ +# Challenge 4 - Run and extend test coverage, and review code + +**[Home](../Readme.md)** - [Previous Challenge](challenge-03.md) - [Next Challenge](challenge-05.md) + +## Goal + +Bring the testing and review of the application up to Octocat standards by running the existing tests, extending coverage, and performing an agent-assisted code review. The mindset shift is from "does it work?" to "can I trust it?". + +## Actions + +* **Run the existing tests** — Execute the current API and frontend test suites and confirm the baseline passes. +* **Find the gaps** — Use Copilot to identify untested code paths, especially around the new cart and backlog features. +* **Extend coverage** — Add unit tests for repository/business logic and component tests for critical UI paths. Include edge cases and error handling. +* **Review the code** — Use Copilot (and/or the code review agent) to review recent changes for correctness, security, and data integrity. +* **Fix what you find** — Address issues surfaced by the tests and review, prioritising security and correctness over style. + +## Success criteria + +* Existing test suites run green. +* New tests meaningfully increase coverage of the recent features and exercise edge cases and error handling. +* A code review has been performed and actionable findings addressed. + +> Repo rule: only run linters, builds, and tests that **already exist** in the repo — no new test frameworks mid-hack. + +### Optional stretch — model selection & context budgeting + +* **Compare models** — Generate tests for the same code once in **Auto** mode and once with an **explicitly selected model**. Compare and record a decision rule for when to use each. +* **Set a context budget** — Before prompting, decide a budget (max number of files, max reference length per file) and stick to it. +* **Note the trade-off** — Capture how the budget affected quality vs. noise in the generated tests. + +## Learning resources + +* [Configuring GitHub Copilot code review](https://docs.github.com/en/copilot/using-github-copilot/code-review/using-copilot-code-review) +* [Writing tests with GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/guides-on-using-github-copilot/writing-tests-with-github-copilot) +* [Changing the AI model for Copilot Chat](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-05.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-05.md new file mode 100644 index 000000000..4282d8b29 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-05.md @@ -0,0 +1,32 @@ +# Challenge 5 - Deploy into Azure + +**[Home](../Readme.md)** - [Previous Challenge](challenge-04.md) - [Next Challenge](challenge-06.md) + +## Goal + +Deploy the full Octocat Supply application to **Microsoft Azure** using **Infrastructure as Code (IaC)** and a **GitHub Actions** CI/CD pipeline. The deployment should be automated, repeatable, defined as code, and driven from GitHub Actions. + +## Actions + +* **Choose your IaC** — Define the Azure infrastructure using an IaC approach (e.g. **Bicep** or **Terraform**) for the resources the app needs: compute for frontend/API, storage/database, and supporting services. +* **Author the templates** — Use Copilot to help generate and refine the IaC, following Azure best practices. +* **Build the pipeline** — Create a GitHub Actions workflow that builds, tests, provisions infrastructure, and deploys the frontend and API. +* **Configure securely** — Use GitHub secrets / **OIDC federated credentials** for Azure authentication; avoid hard-coded credentials. +* **Deploy and verify** — Run the pipeline and confirm the application is reachable in Azure. + +> **Optional starter scaffold (included in this folder).** [`infra/`](../infra/README.md) (Bicep) — bundled here — plus the bundled [`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml) sketch a **Container Apps + ACR + OIDC** topology so you don't start from a blank page. The meaningful design choices — SKUs, database/storage strategy, API ingress visibility, secrets, scaling — are intentionally left as `TODO`s. It removes boilerplate; it does **not** do the challenge for you. Use it, extend it, or replace it. See [`infra/README.md`](../infra/README.md). + +## Success criteria + +* Azure infrastructure is defined as code and provisions cleanly (no manual portal clicking to create resources). +* A single GitHub Actions workflow builds, tests, provisions, and deploys the application. +* Authentication to Azure uses secrets/OIDC — no credentials in source. +* The deployed application is reachable and functional in Azure (exercise a real flow, e.g. the cart). + +## Learning resources + +* [Deploy to Azure from GitHub Actions](https://learn.microsoft.com/en-us/azure/developer/github/github-actions) +* [Authenticate to Azure from GitHub Actions using OIDC](https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect) +* [Bicep documentation](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/) +* [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/) +* [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-06.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-06.md new file mode 100644 index 000000000..0ae866ec9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-06.md @@ -0,0 +1,32 @@ +# Challenge 6 - Monitor the deployed app with the Azure SRE Agent + +**[Home](../Readme.md)** - [Previous Challenge](challenge-05.md) - [Next Challenge](challenge-07.md) + +## Goal + +Use agents to monitor the deployed Octocat Supply application and improve its reliability, leveraging the **Azure SRE Agent**. Now that the app is deployed, the team wants agentic monitoring — detect, diagnose, and help remediate issues. + +> This challenge depends on a reachable deployment from Challenge 5. If your team doesn't have one, pair with another team or use a shared reference deployment so you can still practise the loop. + +## Actions + +* **Enable observability** — Confirm the deployed app emits the telemetry needed for monitoring (logs, metrics, health checks). Add health/readiness endpoints if it doesn't already expose them. +* **Set up the SRE Agent** — Connect the Azure SRE Agent to the deployed resources so it can observe application health. +* **Simulate an issue** — Introduce or trigger a fault (e.g. a failing dependency or a bad deploy) that clearly affects health. +* **Investigate with the agent** — Use the SRE Agent to identify root cause and suggest remediation. +* **Remediate** — Apply the fix and confirm the agent reports healthy status. + +## Success criteria + +* The deployed application emits usable telemetry for monitoring. +* The Azure SRE Agent is monitoring the application. +* An induced issue is detected and diagnosed with agent assistance (not just noticed manually). +* The issue is remediated and health is restored. + +> Capture **what the agent surfaced vs. what you had to investigate manually**, and note reliability improvements to feed back into the backlog. + +## Learning resources + +* [Azure SRE Agent documentation](https://learn.microsoft.com/en-us/azure/sre-agent/) +* [Azure Monitor overview](https://learn.microsoft.com/en-us/azure/azure-monitor/overview) +* [Health probes in Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/health-probes) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-07.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-07.md new file mode 100644 index 000000000..7288a4ce7 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-07.md @@ -0,0 +1,35 @@ +# Challenge 7 - Implement agentic workflows to maintain the app + +**[Home](../Readme.md)** - [Previous Challenge](challenge-06.md) - [Next Challenge](finish.md) + +## Goal + +Implement **GitHub Agentic Workflows** to help maintain the Octocat Supply application over time. Delivering the app is only the beginning — the team needs sustainable, agent-driven maintenance: triaging issues, keeping dependencies current, and responding to changes in the repository. + +## Actions + +* **Identify maintenance tasks** — Choose a recurring task that benefits from automation (e.g. issue triage/labelling, dependency updates, test/lint on change, documentation upkeep). Start with a **low-risk** task. +* **Author an agentic workflow** — Create a GitHub Agentic Workflow that performs one of these tasks autonomously. +* **Define triggers and guardrails** — Configure when it runs (schedule, PR, issue) and constrain what it is allowed to do — least privilege. +* **Test the workflow** — Trigger it and observe the agent completing the task. +* **Review the output** — Confirm the agent's actions are correct and safe before enabling it broadly. + +## Success criteria + +* At least one agentic workflow is implemented and completes a real run. +* The workflow has clear triggers and appropriate, least-privilege guardrails. +* The agent completes a real maintenance task on the repository (not a no-op). +* The workflow's output is reviewed for correctness and safety. + +### Optional stretch — package a reusable skill / slash command + +* Pick a repeatable flow you've done by hand (e.g. "read instructions → scaffold tests → run validation"). +* Package it as a reusable **skill or slash command** so it can be invoked in one step. +* Invoke it against a real task and confirm it performs the steps. +* Document where **human review stays mandatory** (e.g. before merging generated tests or committing changes). + +## Learning resources + +* [GitHub Agentic Workflows (gh-aw)](https://github.com/githubnext/gh-aw) +* [About GitHub Actions](https://docs.github.com/en/actions/learn-github-actions) +* [Controlling permissions for GITHUB_TOKEN](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/finish.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/finish.md new file mode 100644 index 000000000..ac69c1658 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/finish.md @@ -0,0 +1,15 @@ +# Finish + +[Previous Challenge](challenge-07.md) - **[Home](../Readme.md)** + +## Congratulations + +You finished the **Agentic SDLC MicroHack**! 🎉 + +Over the course of the day your team took the **Octocat Supply** application through the complete Agentic Software Development Lifecycle — you **planned**, **built**, **tested**, **reviewed**, **deployed** to Azure, **monitored** with the Azure SRE Agent, and set up agentic workflows to **maintain** it over time. Along the way you practised driving agents deliberately: structured prompting, context budgeting, model selection, requirement refinement from organizational signal, and least-privilege automation. + +The point was never just to write code faster — it was to work *with* agents across the whole lifecycle while keeping changes scoped, tested, secure, and reviewable. + +If you want to give feedback, please don't hesitate to open an Issue on the repository or get in touch with one of the contributors directly. + +Thank you for investing the time — and see you next time! 🚀 diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/README.md new file mode 100644 index 000000000..f6a740971 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/README.md @@ -0,0 +1,144 @@ +# `infra/` — Challenge 5 starter scaffold (optional) + +> **Optional starting point, not a required or "correct" answer.** This is a +> deliberately incomplete **skeleton** for [Challenge 5 — Deploy into Azure](../challenges/challenge-05.md). +> It removes blank-page boilerplate (structure, parameters, module wiring, OIDC +> plumbing) so your 60 minutes go into the interesting agentic work — authoring +> and refining the deploy with Copilot, deploying, and debugging — **not** +> scaffolding. The meaningful engineering decisions are left as clearly-marked +> `// TODO`s on purpose. Extend it, replace it, or ignore it. + +## Intended topology + +Azure **Container Apps** as the compute target: + +``` + Azure Container Registry (ACR) + │ (holds api + frontend images) + ▼ + ┌──────────── Container Apps Environment ────────────┐ + │ │ + │ frontend (nginx) api (Node/Express) │ + │ external ingress :80 ────► ingress :3000 │ + │ proxies /api via nginx (internal OR external)│ + │ │ + └────────────────────┬───────────────────────────────┘ + │ logs + ▼ + Log Analytics workspace +``` + +- **ACR** — stores the two images the apps pull. +- **Log Analytics workspace** — a hard dependency of the Container Apps + environment; provisioned first. +- **Container Apps Environment** — the shared boundary both apps run in. Apps in + the same environment can address each other by app name (service discovery). +- **`api` Container App** — target port **3000** (matches `src/api-ts/Dockerfile`, + which `EXPOSE`s 3000 and runs `npm start`). +- **`frontend` Container App** — external ingress on port **80** (matches + `src/frontend/Dockerfile`, nginx). It is configured with `API_HOST` / + `API_PORT` env vars pointing at the `api` app so nginx can reverse-proxy `/api` + (see `src/frontend/nginx.conf` + `src/frontend/entrypoint.sh`). + +## How the Bicep maps to the two Dockerfiles + +| Dockerfile | Port | Container App | Notes | +| --- | --- | --- | --- | +| `src/api-ts/Dockerfile` | `EXPOSE 3000` | `--api` | `targetPort: 3000`. Ingress internal-vs-external is a **TODO** decision. | +| `src/frontend/Dockerfile` | `EXPOSE 80` | `--frontend` | `targetPort: 80`, external. Gets `API_HOST` = the api app name and `API_PORT` = `3000`. The entrypoint also honours `API_PROTOCOL` (default `https`). | + +The workflow builds each image from its own context (`src/api-ts` and +`src/frontend`) and pushes to ACR; the Bicep then references those image tags. + +## Files + +| File | Purpose | +| --- | --- | +| `main.bicep` | `targetScope = 'resourceGroup'`. Wires ACR, Log Analytics, the environment, and the two apps by calling the modules. Declares parameters + outputs. | +| `modules/registry.bicep` | ACR (stub — SKU/admin/network are TODO). | +| `modules/loganalytics.bicep` | Log Analytics workspace (stub — retention/SKU TODO). | +| `modules/containerapp-env.bicep` | Container Apps managed environment. | +| `modules/containerapp.bicep` | Generic app module, reused for **both** api and frontend. | +| `main.parameters.json` | Placeholder parameter values with TODO comments. | + +## What's provided vs. deliberately left as `TODO` + +**Provided (so you don't start from a blank page):** + +- Module structure and the wiring between them. +- Sensible parameters (`location`, `namePrefix`, `environmentName`, image + references, `minReplicas` / `maxReplicas`) and outputs (ACR login server, + frontend URL, api FQDN). +- The port mapping (api `3000`, frontend `80`) and the `API_HOST` / `API_PORT` + proxy wiring between frontend and api. +- OIDC-based CI/CD plumbing in the workflow (no long-lived secrets). + +**Left as `TODO` (the decisions that make this a real deploy — do these):** + +- **SKUs** — ACR tier, Log Analytics retention, container CPU/memory. +- **api ingress visibility** — public (its own URL, easy debugging) vs. + internal-only (reached solely by the frontend inside the environment). +- **Database / storage strategy** — the api ships with an in-container + **SQLite** DB. In Container Apps that is **ephemeral** (it resets on every + revision/scale event). Choose one: keep it ephemeral for a demo, mount an + **Azure Files** volume to persist it, or move to a **managed DB** + (Azure SQL / PostgreSQL) and inject a connection string. *Not decided for you.* +- **Registry auth** — the scaffold defaults to ACR **admin credentials** for a + fast first deploy; the preferred approach is a **user-assigned managed + identity** with the `AcrPull` role (no secrets). Swap it in. +- **Env vars / secrets** — real app configuration and how it's referenced. +- **Scaling rules** — replica counts are set, but add real scale triggers + (HTTP concurrency, CPU, ...). +- **Service discovery** — confirm the frontend→api wiring works with whichever + ingress visibility you pick. + +## Deploy it manually + +```bash +# 1. Sanity-check the template compiles +az bicep build --file infra/main.bicep # or: bicep build infra/main.bicep + +# 2. Create a resource group +az group create --name --location + +# 3. Deploy (fill in the image references — see main.parameters.json TODOs) +az deployment group create \ + --resource-group \ + --template-file infra/main.bicep \ + --parameters infra/main.parameters.json \ + --parameters \ + apiImage=".azurecr.io/api:" \ + frontendImage=".azurecr.io/frontend:" + +# 4. Read the outputs (e.g. the public frontend URL) +az deployment group show -g -n main \ + --query properties.outputs.frontendUrl.value -o tsv +``` + +> **Chicken-and-egg note:** the images live in the ACR this template creates, so +> on a first run either point `apiImage` / `frontendImage` at a temporary public +> placeholder image, deploy once to create the ACR, then build/push and redeploy +> — **or** split provisioning: create the ACR first, push images, then deploy the +> apps. The workflow leaves this ordering as a TODO for you to decide. + +## Deploy it via the workflow + +[`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml) is an +OIDC-wired scaffold with `# TODO` steps (checkout → `azure/login` → build/push → +`az group create` → `az deployment group create` → update apps). It is **inert +by default**: the `push` trigger is commented out and it needs the OIDC secrets +below. Set those up, finish the TODO steps with Copilot, then run it from the +**Actions** tab (`workflow_dispatch`). + +Required GitHub **secrets**: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, +`AZURE_SUBSCRIPTION_ID`. Required **variables**: `AZURE_RESOURCE_GROUP`, +`AZURE_LOCATION`. See the header comment in the workflow for the federated- +credential subject (`repo:/:ref:refs/heads/main`). No client secret +is stored — auth is OIDC only. **Setting up the OIDC federated credential is the +single most common stumbling block — do it first and verify with a trivial login.** + +## Tooling note + +The repo's `.vscode/mcp.json` preconfigures an **Azure MCP Server** +(`@azure/mcp`). You can use it during the challenge to explore/verify Azure +resources — it's handy but not required by this scaffold. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.bicep b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.bicep new file mode 100644 index 000000000..b75d3da04 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.bicep @@ -0,0 +1,193 @@ +// ============================================================================= +// main.bicep — Challenge 5 starter scaffold (OPTIONAL, SKELETON — not an answer key) +// ----------------------------------------------------------------------------- +// Target architecture expressed here (Azure Container Apps): +// +// ACR ──images──> ┌─────────────── Container Apps Environment ───────────────┐ +// │ frontend (nginx, :80, external) ──proxy──> api (:3000) │ +// └───────────────────────────────────────────────────────────┘ +// │ logs +// Log Analytics workspace +// +// The frontend's nginx proxies /api to the api app using API_HOST / API_PORT +// (see src/frontend/entrypoint.sh + nginx.conf). Within one Container Apps +// environment, apps can address each other by app name, which is what makes +// that service discovery work. +// +// WHAT'S PROVIDED: the wiring, parameters, module structure, outputs. +// WHAT'S LEFT TO YOU (look for // TODO): SKUs, ingress visibility for the api, +// exact env/secrets, scaling rules, and the database/storage strategy. +// +// Deploy manually: +// az group create -n -l +// az deployment group create -g -f infra/main.bicep -p infra/main.parameters.json +// ============================================================================= + +targetScope = 'resourceGroup' + +@description('Azure region for all resources. Defaults to the resource group location.') +param location string = resourceGroup().location + +@description('Short prefix used to derive resource names (lowercase letters/numbers).') +@minLength(2) +@maxLength(12) +param namePrefix string = 'octocat' + +@description('Environment name / suffix (e.g. dev, test, hack). Keeps names unique per env.') +param environmentName string = 'dev' + +@description('Container image reference for the API (e.g. .azurecr.io/api:).') +param apiImage string + +@description('Container image reference for the frontend (e.g. .azurecr.io/frontend:).') +param frontendImage string + +@description('Minimum replicas for each app.') +@minValue(0) +param minReplicas int = 1 + +@description('Maximum replicas for each app.') +@minValue(1) +param maxReplicas int = 3 + +// TODO: The scaffold passes ACR admin credentials into the frontend/api modules +// for a quick first deploy. Prefer a user-assigned managed identity with +// AcrPull and remove these entirely. Left as params so nothing is baked in. +@description('ACR admin username (leave empty to switch modules to managed identity).') +param registryUsername string = '' + +@description('ACR admin password (leave empty to switch modules to managed identity).') +@secure() +param registryPassword string = '' + +// ----------------------------------------------------------------------------- +// Derived names. Registry names must be globally unique and alphanumeric only. +// ----------------------------------------------------------------------------- +var baseName = '${namePrefix}-${environmentName}' +var registryName = toLower(replace('${namePrefix}${environmentName}acr', '-', '')) +var logAnalyticsName = '${baseName}-logs' +var environmentResourceName = '${baseName}-env' +var apiAppName = '${baseName}-api' +var frontendAppName = '${baseName}-frontend' + +var commonTags = { + project: 'octocat-supply' + challenge: 'challenge-4-azure-deploy' + managedBy: 'bicep' + environment: environmentName +} + +// ----------------------------------------------------------------------------- +// Container Registry +// ----------------------------------------------------------------------------- +module registry 'modules/registry.bicep' = { + name: 'registry' + params: { + location: location + registryName: registryName + tags: commonTags + } +} + +// ----------------------------------------------------------------------------- +// Log Analytics (dependency of the Container Apps environment) +// ----------------------------------------------------------------------------- +module logAnalytics 'modules/loganalytics.bicep' = { + name: 'loganalytics' + params: { + location: location + workspaceName: logAnalyticsName + tags: commonTags + } +} + +// ----------------------------------------------------------------------------- +// Container Apps managed environment +// ----------------------------------------------------------------------------- +module environment 'modules/containerapp-env.bicep' = { + name: 'containerapp-env' + params: { + location: location + environmentName: environmentResourceName + logAnalyticsWorkspaceId: logAnalytics.outputs.workspaceId + tags: commonTags + } +} + +// ----------------------------------------------------------------------------- +// API container app — listens on 3000. +// ----------------------------------------------------------------------------- +// TODO: Ingress visibility decision. `externalIngress: true` gives the api its +// own public URL (handy for debugging/tests). Set it to false to make the +// api internal-only and reachable solely by the frontend inside the env — +// usually the more correct production choice. Pick one deliberately. +module apiApp 'modules/containerapp.bicep' = { + name: 'api-app' + params: { + location: location + appName: apiAppName + environmentId: environment.outputs.environmentId + image: apiImage + targetPort: 3000 + externalIngress: true + registryLoginServer: registry.outputs.loginServer + registryUsername: registryUsername + registryPassword: registryPassword + minReplicas: minReplicas + maxReplicas: maxReplicas + tags: commonTags + // TODO: Add the api's runtime env vars / secrets here (e.g. DB connection + // string once you pick a database strategy). + env: [] + } +} + +// ----------------------------------------------------------------------------- +// Frontend container app — nginx on 80, external, proxying to the api. +// ----------------------------------------------------------------------------- +// The API_HOST/API_PORT env vars tell nginx where the api lives. Using the api +// app name resolves inside the environment for service discovery. +module frontendApp 'modules/containerapp.bicep' = { + name: 'frontend-app' + params: { + location: location + appName: frontendAppName + environmentId: environment.outputs.environmentId + image: frontendImage + targetPort: 80 + externalIngress: true + registryLoginServer: registry.outputs.loginServer + registryUsername: registryUsername + registryPassword: registryPassword + minReplicas: minReplicas + maxReplicas: maxReplicas + tags: commonTags + env: [ + { + name: 'API_HOST' + value: apiAppName + } + { + name: 'API_PORT' + // TODO: If you make the api internal-only, confirm the frontend still + // reaches it on 3000 within the environment (it should). If you + // front the api on 443/https externally, adjust API_PORT/PROTOCOL. + value: '3000' + } + // TODO: The entrypoint also honours API_PROTOCOL (default https). Add it + // here if your ingress/scheme requires it. + ] + } +} + +// ----------------------------------------------------------------------------- +// Outputs +// ----------------------------------------------------------------------------- +@description('ACR login server — push images here and reference them in the apps.') +output acrLoginServer string = registry.outputs.loginServer + +@description('Public URL of the frontend app.') +output frontendUrl string = 'https://${frontendApp.outputs.fqdn}' + +@description('FQDN of the api app (public only while externalIngress is true).') +output apiFqdn string = apiApp.outputs.fqdn diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.parameters.json b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.parameters.json new file mode 100644 index 000000000..8b334a581 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/main.parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "_comment_location": "TODO: Set a region with Container Apps capacity (e.g. eastus, westeurope). Defaults to the resource group location if omitted.", + "namePrefix": { + "value": "octocat" + }, + "environmentName": { + "value": "dev" + }, + "_comment_images": "TODO: Replace with your registry login server once the ACR name is known. The workflow builds/pushes these tags. On a first bootstrap you may point at a public placeholder image until the real images exist.", + "apiImage": { + "value": ".azurecr.io/api:latest" + }, + "frontendImage": { + "value": ".azurecr.io/frontend:latest" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 3 + }, + "_comment_registry_creds": "TODO: These enable the simplest ACR-admin-credentials path for a first deploy. Prefer a user-assigned managed identity with AcrPull and leave these empty. Never commit real values — pass them at deploy time (e.g. from the workflow) instead.", + "registryUsername": { + "value": "" + }, + "registryPassword": { + "value": "" + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp-env.bicep b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp-env.bicep new file mode 100644 index 000000000..83334ee6a --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp-env.bicep @@ -0,0 +1,50 @@ +// ============================================================================= +// Module: containerapp-env.bicep (Azure Container Apps managed environment) +// ----------------------------------------------------------------------------- +// The shared environment both apps (api + frontend) run in. Apps in the same +// environment can reach each other by app name for service discovery — this is +// what lets the frontend's nginx proxy to the api (see API_HOST/API_PORT). +// +// STARTER SKELETON — compiles as-is; decisions left as // TODO. +// ============================================================================= + +@description('Azure region for the environment.') +param location string + +@description('Name of the Container Apps managed environment.') +param environmentName string + +@description('Resource ID of the Log Analytics workspace to send logs to.') +param logAnalyticsWorkspaceId string + +@description('Tags applied to the environment.') +param tags object = {} + +// Derive the workspace customerId + shared key from the passed-in workspace id. +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { + name: last(split(logAnalyticsWorkspaceId, '/')) +} + +resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: environmentName + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + // TODO: Decide whether you need a custom VNet (internal-only environment), + // zone redundancy, or workload profiles (Consumption vs Dedicated). + // The default is the public, Consumption-only profile — fine to start. + } +} + +@description('The resource ID of the managed environment.') +output environmentId string = environment.id + +@description('The default domain of the environment (apps get .).') +output defaultDomain string = environment.properties.defaultDomain diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp.bicep b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp.bicep new file mode 100644 index 000000000..bae5bab8e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/containerapp.bicep @@ -0,0 +1,141 @@ +// ============================================================================= +// Module: containerapp.bicep (generic Container App — reused for api + frontend) +// ----------------------------------------------------------------------------- +// One reusable module for BOTH workloads: +// - api: target port 3000, ingress (internal or external — a TODO decision) +// - frontend: target port 80, external ingress, with API_HOST/API_PORT env +// vars pointing at the api app so nginx can proxy /api. +// +// STARTER SKELETON — compiles as-is; decisions left as // TODO. +// ============================================================================= + +@description('Azure region for the app.') +param location string + +@description('Name of the container app (also its DNS label within the environment).') +param appName string + +@description('Resource ID of the Container Apps managed environment.') +param environmentId string + +@description('Fully-qualified image reference, e.g. myregistry.azurecr.io/api:latest.') +param image string + +@description('The port the container listens on (api=3000, frontend=80).') +param targetPort int + +@description('Whether ingress is exposed externally (public). Set false for internal-only.') +param externalIngress bool = true + +@description('Login server of the ACR the image is pulled from.') +param registryLoginServer string + +@description('Environment variables for the container. Array of { name, value } objects.') +param env array = [] + +@description('Tags applied to the app.') +param tags object = {} + +// TODO: Scaling. These defaults keep at least one replica warm. For a scale-to- +// zero API set minReplicas: 0, and add custom scale rules (HTTP +// concurrency, CPU, queue length, ...) rather than relying on replica +// counts alone. +@minValue(0) +param minReplicas int = 1 + +@minValue(1) +param maxReplicas int = 3 + +// TODO: Right-size CPU/memory. 0.5 vCPU / 1Gi is a reasonable starting point; +// valid combinations are constrained (see Container Apps docs). +param cpu string = '0.5' +param memory string = '1.0Gi' + +// ----------------------------------------------------------------------------- +// Registry authentication +// ----------------------------------------------------------------------------- +// TODO: This scaffold authenticates to ACR with the admin username/password +// passed in as a secret (simplest path to a first deploy). The PREFERRED +// approach is a user-assigned managed identity with the AcrPull role and +// `identity: ''` on the registry entry — no +// secrets at all. Swap this out once you wire up the identity + role. +@description('ACR admin username (leave empty when using managed identity).') +param registryUsername string = '' + +@description('ACR admin password (leave empty when using managed identity).') +@secure() +param registryPassword string = '' + +var useAdminCreds = !empty(registryUsername) + +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: appName + location: location + tags: tags + properties: { + managedEnvironmentId: environmentId + configuration: { + ingress: { + external: externalIngress + targetPort: targetPort + // TODO: For the api you may want `transport: 'http'` and to restrict + // traffic; for the frontend the defaults are usually fine. + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + } + // Only attach registry credentials when using admin creds. With a managed + // identity you would set `identity` here and omit the secret. + registries: useAdminCreds ? [ + { + server: registryLoginServer + username: registryUsername + passwordSecretRef: 'registry-password' + } + ] : [] + secrets: useAdminCreds ? [ + { + name: 'registry-password' + value: registryPassword + } + ] : [] + // TODO: Add application secrets here (DB connection strings, API keys, ...) + // and reference them from env via secretRef instead of value. + } + template: { + containers: [ + { + name: appName + image: image + resources: { + cpu: json(cpu) + memory: memory + } + env: env + // TODO: Add liveness/readiness probes so revisions only take traffic + // once healthy (e.g. GET / on the frontend, a health route on + // the api). Container Apps supports `probes: [...]` here. + } + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + // TODO: Add `rules: [...]` for real autoscaling triggers. + } + // TODO: Database/storage decision. If you keep the api's SQLite DB inside + // the container it is EPHEMERAL — it resets on every revision/scale + // event. To persist, either mount an Azure Files volume here + // (`volumes` + `volumeMounts`) or move to a managed DB (Azure SQL / + // Postgres) and inject a connection string. This is a team decision. + } + } +} + +@description('The fully-qualified domain name of the app (empty when ingress is disabled).') +output fqdn string = containerApp.properties.configuration.ingress.fqdn + +@description('The name of the container app.') +output appName string = containerApp.name diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/loganalytics.bicep b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/loganalytics.bicep new file mode 100644 index 000000000..5e7471a52 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/loganalytics.bicep @@ -0,0 +1,43 @@ +// ============================================================================= +// Module: loganalytics.bicep (Log Analytics workspace) +// ----------------------------------------------------------------------------- +// The Container Apps Environment sends logs here. It is a hard dependency of the +// managed environment, so we provision it first and pass its id/key upstream. +// +// STARTER SKELETON — compiles as-is; decisions left as // TODO. +// ============================================================================= + +@description('Azure region for the workspace.') +param location string + +@description('Name of the Log Analytics workspace.') +param workspaceName string + +@description('Tags applied to the workspace.') +param tags object = {} + +// TODO: Tune retention for cost vs. how long you need logs during the event. +// 30 days is the free-tier default. +@minValue(30) +@maxValue(730) +param retentionInDays int = 30 + +resource workspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: workspaceName + location: location + tags: tags + properties: { + // TODO: PerGB2018 is the standard pay-as-you-go SKU. Revisit if you have a + // committed-tier or a capacity reservation. + sku: { + name: 'PerGB2018' + } + retentionInDays: retentionInDays + } +} + +@description('The resource ID of the Log Analytics workspace.') +output workspaceId string = workspace.id + +@description('The customer/workspace GUID used by the Container Apps environment.') +output customerId string = workspace.properties.customerId diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/registry.bicep b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/registry.bicep new file mode 100644 index 000000000..7acb6f7a0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/infra/modules/registry.bicep @@ -0,0 +1,56 @@ +// ============================================================================= +// Module: registry.bicep (Azure Container Registry) +// ----------------------------------------------------------------------------- +// Holds the two application images (api, frontend) that the Container Apps pull. +// +// This is a STARTER SKELETON. The core resource + key params are wired so the +// template compiles, but the meaningful decisions are left as // TODO for you. +// ============================================================================= + +@description('Azure region for the registry.') +param location string + +@description('Globally-unique name for the container registry (alphanumeric, 5-50 chars).') +param registryName string + +@description('Tags applied to the registry.') +param tags object = {} + +// TODO: Decide the SKU. Basic is cheapest and fine for a hackathon; Standard/ +// Premium add throughput, geo-replication, private endpoints, etc. +// Ask Copilot for Well-Architected guidance if unsure. +@allowed([ + 'Basic' + 'Standard' + 'Premium' +]) +param sku string = 'Basic' + +// TODO: For OIDC/managed-identity pulls you can (and ideally should) keep the +// admin user DISABLED and grant the Container Apps' identity AcrPull +// instead. The scaffold defaults to enabled to keep first-run simple — +// flip this to false once you wire up role assignments. +param adminUserEnabled bool = true + +resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { + name: registryName + location: location + tags: tags + sku: { + name: sku + } + properties: { + adminUserEnabled: adminUserEnabled + // TODO: Consider `publicNetworkAccess: 'Disabled'` + private endpoints for + // a hardened setup, and `anonymousPullEnabled: false`. + } +} + +@description('The login server host name of the registry (e.g. myregistry.azurecr.io).') +output loginServer string = registry.properties.loginServer + +@description('The resource ID of the registry (useful for role assignments).') +output registryId string = registry.id + +@description('The name of the registry.') +output registryName string = registry.name diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/README.md new file mode 100644 index 000000000..fbb1f4ba6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/README.md @@ -0,0 +1,69 @@ +# Scripts + +Utility scripts for running the Agentic SDLC Hackathon. + +## `seed-backlog.sh` / `seed-backlog.ps1` — Seed the Challenge 3 backlog + +These scripts create the [Challenge 3 backlog](../assets/backlog.md) as real **GitHub Issues** in a +repo, so teams pick from a real, prioritised, labelled list instead of inventing a backlog. The seeded +issues are also what an MCP server can pull as **real backlog context** in the Challenge 3 optional stretch. + +Both scripts do the same thing — use the `.sh` on macOS/Linux and the `.ps1` on Windows. + +### What they do + +- Ensure the labels `backlog`, `priority:high`, `priority:medium`, and `priority:low` exist (created if + missing; existing labels are tolerated). +- Create one issue per backlog item, with a title, a body derived from `assets/backlog.md` + (description + acceptance criteria + affected layers), and the `backlog` + priority labels. +- Skip any item whose title already matches an **open** issue, so the scripts are safe to re-run. +- Print a summary of labels ensured, issues created, and issues skipped. + +By default **8 issues** are seeded — one per item in `assets/backlog.md`. To change the set, edit the +backlog items in the script (and keep it in sync with `assets/backlog.md`). + +### Prerequisites + +- **GitHub CLI** installed — https://cli.github.com/ +- **Authenticated** with `repo` scope — `gh auth login` + +If `gh` is missing or unauthenticated, the scripts stop with a clear, actionable message before making +any changes. + +### Usage + +**Bash (macOS / Linux):** + +```bash +# Seed the current repo (the one gh resolves for this directory) +./scripts/seed-backlog.sh + +# Target a specific team repo or fork +./scripts/seed-backlog.sh --repo octo-org/team-1 + +# Preview only — prints what would be created, no API calls +./scripts/seed-backlog.sh --dry-run +``` + +**PowerShell (Windows):** + +```powershell +# Seed the current repo +./scripts/seed-backlog.ps1 + +# Target a specific team repo or fork +./scripts/seed-backlog.ps1 -Repo octo-org/team-1 + +# Preview only — no API calls +./scripts/seed-backlog.ps1 -DryRun +``` + +> On macOS/Linux you may need to make the script executable first: `chmod +x scripts/seed-backlog.sh`. + +### When to run it (event flow) + +- **Pre-event, per team repo/fork.** As part of pre-event repo prep, run the seeder against each team's repo/fork so the backlog is already there + when teams reach Challenge 3. +- **Do a `--dry-run` first** to confirm the target repo and see the list before creating anything. +- It's safe to re-run if a repo was missed or you want to top up after manual edits — duplicates are + skipped by title. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.ps1 b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.ps1 new file mode 100644 index 000000000..3c6258d31 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.ps1 @@ -0,0 +1,262 @@ +<# +.SYNOPSIS + Seed the Challenge 3 backlog into a GitHub repo as Issues. + +.DESCRIPTION + Creates one GitHub Issue per item in assets/backlog.md, so teams can pick from a real, + prioritised, labelled list instead of inventing a backlog. The same issues are what an MCP + server can pull as real backlog context in the Challenge 3 optional stretch. + + Labels (backlog, priority:high|medium|low) are ensured first. The script is safe to re-run: + open issues with an identical title are skipped rather than duplicated. + +.PARAMETER Repo + Target a specific repo or fork (e.g. a team repo), in form. + Defaults to the repo gh resolves for the current directory. + +.PARAMETER DryRun + Print what WOULD be created without calling the GitHub API. + +.NOTES + PREREQUISITES + - GitHub CLI installed: https://cli.github.com/ + - Authenticated: gh auth login (needs 'repo' scope to create issues/labels) + +.EXAMPLE + ./scripts/seed-backlog.ps1 + Seed the current repo. + +.EXAMPLE + ./scripts/seed-backlog.ps1 -Repo octo-org/team-1 + Seed a specific team repo/fork. + +.EXAMPLE + ./scripts/seed-backlog.ps1 -DryRun + Preview only — no API calls. +#> +[CmdletBinding()] +param( + [string]$Repo = "", + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +# gh --repo args (empty when targeting the current repo) +$RepoArgs = @() +if ($Repo -ne "") { + $RepoArgs = @("--repo", $Repo) +} + +# --------------------------------------------------------------------------- +# Pre-flight checks: gh installed + authenticated +# --------------------------------------------------------------------------- +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Error "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and re-run this script." + exit 1 +} + +gh auth status *> $null +if ($LASTEXITCODE -ne 0) { + Write-Error "GitHub CLI is not authenticated. Run 'gh auth login' (with 'repo' scope) and re-run this script." + exit 1 +} + +# --------------------------------------------------------------------------- +# Label definitions +# --------------------------------------------------------------------------- +$Labels = @( + @{ Name = "backlog"; Color = "0e8a16"; Description = "Challenge 3 backlog item" }, + @{ Name = "priority:high"; Color = "b60205"; Description = "High priority" }, + @{ Name = "priority:medium"; Color = "fbca04"; Description = "Medium priority" }, + @{ Name = "priority:low"; Color = "0e8a16"; Description = "Low priority" } +) + +# --------------------------------------------------------------------------- +# Backlog items (kept in sync with assets/backlog.md and seed-backlog.sh) +# --------------------------------------------------------------------------- +$Items = @( + @{ + Title = "Payment integration for the cart" + Priority = "High"; Size = "M"; Layers = "frontend, API" + Body = "As a customer, I want to pay for the items in my cart so that I can complete a purchase and turn my cart into a confirmed order. This is the natural continuation of the cart feature from Challenge 2." + Ac = @( + "A customer can start a checkout from the cart and submit payment details through a dedicated flow.", + "A successful payment converts the cart into an order (with order details) via the API.", + "Payment configuration (keys/provider) is read from environment variables — no secrets in source.", + "Failed or declined payments surface a clear error and leave the cart intact." + ) + }, + @{ + Title = "Order history" + Priority = "Medium"; Size = "M"; Layers = "frontend, API" + Body = "As a customer, I want to see a list of my past orders and open any one of them so that I can review what I bought and when." + Ac = @( + "The API exposes an endpoint that returns orders (with their order details) for a customer/branch.", + "The frontend shows an order-history list with date, status, and total per order.", + "Selecting an order shows its line items (product, quantity, unit price).", + "Empty state is handled gracefully when there are no orders yet." + ) + }, + @{ + Title = "Product search & filtering" + Priority = "Medium"; Size = "M"; Layers = "frontend, API" + Body = "As a customer, I want to search and filter the product catalogue so that I can quickly find the products I need instead of scrolling the whole list." + Ac = @( + "The products endpoint accepts query parameters for a text search (name/SKU) and at least one filter (e.g. supplier or price range).", + "The products page has a search box and filter control wired to the API.", + "Results update to reflect the active search/filter, and clearing them restores the full list.", + "Search is case-insensitive and handles ""no results"" cleanly." + ) + }, + @{ + Title = "Inventory management / stock levels" + Priority = "High"; Size = "L"; Layers = "frontend, API, database" + Body = "As a branch manager, I want each product to track a stock level so that customers can't order more than is available and staff can see what needs restocking." + Ac = @( + "Products carry a stock/quantity-on-hand value (schema/migration added following existing patterns).", + "The API returns stock levels and prevents ordering more than the available quantity.", + "The frontend shows stock status (e.g. in stock / low / out of stock) on product views.", + "Placing an order decrements the relevant stock, and the change is covered by tests." + ) + }, + @{ + Title = "Supplier & branch management view" + Priority = "Medium"; Size = "M"; Layers = "frontend, API" + Body = "As an admin, I want to view and manage suppliers and branches so that I can keep the organisation's supply-chain data accurate, mirroring the existing product admin experience." + Ac = @( + "An admin view lists suppliers (and/or branches) using the existing admin UI patterns.", + "An admin can create or edit a supplier/branch through the API.", + "Basic validation is applied (e.g. required name) with consistent error responses.", + "The view is reachable from the existing admin navigation." + ) + }, + @{ + Title = "Delivery tracking & status" + Priority = "Medium"; Size = "M"; Layers = "frontend, API" + Body = "As a branch manager, I want to see deliveries and their status so that I can track incoming stock from suppliers against the orders that need it." + Ac = @( + "The API exposes deliveries with their status and links to the related order details.", + "The frontend shows a deliveries list with supplier, date, and status.", + "A delivery's status can be advanced (e.g. pending → shipped → delivered) via the API.", + "Status changes are validated and reflected in the UI." + ) + }, + @{ + Title = "Order status workflow" + Priority = "Medium"; Size = "S–M"; Layers = "frontend, API" + Body = "As a branch manager, I want to move an order through a clear set of statuses so that everyone can see where each order stands from placement to fulfilment." + Ac = @( + "Orders support a defined set of statuses (e.g. pending → confirmed → shipped → delivered / cancelled).", + "The API only allows valid status transitions and rejects invalid ones with a clear error.", + "The frontend shows the current status and allows an authorised user to advance it.", + "The transition rules are covered by unit tests." + ) + }, + @{ + Title = "Cart quantity validation" + Priority = "Low"; Size = "S"; Layers = "frontend, API" + Body = "As a customer, I want the cart to reject nonsensical quantities so that I can't accidentally order zero, a negative amount, or a non-numeric quantity." + Ac = @( + "Quantities must be positive integers; invalid values are rejected on both the API and the UI.", + "The API returns a consistent validation error for bad quantities.", + "The cart UI shows an inline message and blocks the update/checkout until it's fixed.", + "The validation logic has unit-test coverage." + ) + } +) + +# --------------------------------------------------------------------------- +# Ensure labels exist +# --------------------------------------------------------------------------- +Write-Host "==> Ensuring labels exist..." +$LabelsEnsured = 0 +foreach ($label in $Labels) { + if ($DryRun) { + Write-Host " [dry-run] would ensure label: $($label.Name)" + $LabelsEnsured++ + continue + } + # 'gh label create --force' creates or updates, so it tolerates existing labels. + gh label create $label.Name --color $label.Color --description $label.Description --force @RepoArgs *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host " ensured label: $($label.Name)" + $LabelsEnsured++ + } + else { + Write-Warning "could not ensure label '$($label.Name)' (continuing)" + } +} + +# --------------------------------------------------------------------------- +# Fetch existing open issue titles once (for duplicate detection) +# --------------------------------------------------------------------------- +$ExistingTitles = @() +if (-not $DryRun) { + $raw = gh issue list --state open --limit 200 --json title --jq ".[].title" @RepoArgs 2>$null + if ($LASTEXITCODE -eq 0 -and $raw) { + $ExistingTitles = @($raw -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }) + } +} + +# --------------------------------------------------------------------------- +# Create issues +# --------------------------------------------------------------------------- +Write-Host "==> Seeding backlog issues..." +$Created = 0 +$Skipped = 0 + +foreach ($item in $Items) { + $prioLabel = "priority:" + $item.Priority.ToLower() + + $acBlock = ($item.Ac | ForEach-Object { "- [ ] $_" }) -join "`n" + $issueBody = @" +$($item.Body) + +**Priority:** $($item.Priority) | **Size:** $($item.Size) + +**Affected layers:** $($item.Layers) + +**Acceptance criteria** + +$acBlock + +_Seeded from assets/backlog.md for Challenge 3._ +"@ + + if ($ExistingTitles -contains $item.Title) { + Write-Host " skip (already exists): $($item.Title)" + $Skipped++ + continue + } + + if ($DryRun) { + Write-Host " [dry-run] would create issue: $($item.Title) [backlog, $prioLabel]" + $Created++ + continue + } + + gh issue create --title $item.Title --body $issueBody --label "backlog" --label $prioLabel @RepoArgs *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host " created: $($item.Title)" + $Created++ + } + else { + Write-Warning "failed to create issue '$($item.Title)' (continuing)" + $Skipped++ + } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +$target = if ($Repo -ne "") { $Repo } else { "current repo" } +Write-Host "" +Write-Host "==> Summary ($target)" +if ($DryRun) { + Write-Host " Mode: DRY RUN (no changes made)" +} +Write-Host " Labels ensured: $LabelsEnsured" +Write-Host " Issues created: $Created" +Write-Host " Issues skipped: $Skipped" +Write-Host "Done." diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.sh b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.sh new file mode 100644 index 000000000..83a137700 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/seed-backlog.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash +# +# seed-backlog.sh — Seed the Challenge 3 backlog into a GitHub repo as Issues. +# +# Creates one GitHub Issue per item in assets/backlog.md, so teams can pick +# from a real, prioritised, labelled list instead of inventing a backlog. The +# same issues are what an MCP server can pull as real backlog context in the +# Challenge 3 optional stretch. +# +# PREREQUISITES +# - GitHub CLI installed: https://cli.github.com/ +# - Authenticated: gh auth login (needs 'repo' scope to create issues/labels) +# +# USAGE +# ./scripts/seed-backlog.sh [--repo ] [--dry-run] [-h|--help] +# +# --repo Target a specific repo or fork (e.g. a team repo). +# Defaults to the repo gh resolves for the current directory. +# --dry-run Print what WOULD be created without calling the GitHub API. +# -h, --help Show this help and exit. +# +# EXAMPLES +# ./scripts/seed-backlog.sh # seed the current repo +# ./scripts/seed-backlog.sh --repo octo-org/team-1 # seed a specific team repo/fork +# ./scripts/seed-backlog.sh --dry-run # preview only, no API calls +# +# NOTES +# - Labels (backlog, priority:high|medium|low) are created first if missing. +# - Safe to re-run: existing open issues with an identical title are skipped, not duplicated. +# +set -euo pipefail + +REPO="" +DRY_RUN=false + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) + REPO="${2:-}" + if [[ -z "$REPO" ]]; then + echo "Error: --repo requires an value." >&2 + exit 1 + fi + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Error: unknown argument '$1'. Use --help for usage." >&2 + exit 1 + ;; + esac +done + +# gh --repo flag (empty when targeting the current repo) +REPO_ARGS=() +if [[ -n "$REPO" ]]; then + REPO_ARGS=(--repo "$REPO") +fi + +# --------------------------------------------------------------------------- +# Pre-flight checks: gh installed + authenticated +# --------------------------------------------------------------------------- +if ! command -v gh >/dev/null 2>&1; then + cat >&2 <<'EOF' +Error: GitHub CLI (gh) is not installed. +Install it from https://cli.github.com/ and re-run this script. +EOF + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + cat >&2 <<'EOF' +Error: GitHub CLI is not authenticated. +Run 'gh auth login' (with 'repo' scope) and re-run this script. +EOF + exit 1 +fi + +# --------------------------------------------------------------------------- +# Label definitions: name|color|description +# --------------------------------------------------------------------------- +LABELS=( + "backlog|0e8a16|Challenge 3 backlog item" + "priority:high|b60205|High priority" + "priority:medium|fbca04|Medium priority" + "priority:low|0e8a16|Low priority" +) + +# --------------------------------------------------------------------------- +# Backlog items. Each item is a block of key=value lines separated by a line +# containing only '---'. Keeps the two scripts (bash/ps1) easy to keep in sync +# with assets/backlog.md. +# title Issue title +# priority High|Medium|Low (maps to priority: label) +# size S|M|L +# layers affected layers +# body user-story description +# ac one acceptance criterion (repeatable) +# --------------------------------------------------------------------------- +read -r -d '' ITEMS <<'ITEMS_EOF' || true +title=Payment integration for the cart +priority=High +size=M +layers=frontend, API +body=As a customer, I want to pay for the items in my cart so that I can complete a purchase and turn my cart into a confirmed order. This is the natural continuation of the cart feature from Challenge 2. +ac=A customer can start a checkout from the cart and submit payment details through a dedicated flow. +ac=A successful payment converts the cart into an order (with order details) via the API. +ac=Payment configuration (keys/provider) is read from environment variables — no secrets in source. +ac=Failed or declined payments surface a clear error and leave the cart intact. +--- +title=Order history +priority=Medium +size=M +layers=frontend, API +body=As a customer, I want to see a list of my past orders and open any one of them so that I can review what I bought and when. +ac=The API exposes an endpoint that returns orders (with their order details) for a customer/branch. +ac=The frontend shows an order-history list with date, status, and total per order. +ac=Selecting an order shows its line items (product, quantity, unit price). +ac=Empty state is handled gracefully when there are no orders yet. +--- +title=Product search & filtering +priority=Medium +size=M +layers=frontend, API +body=As a customer, I want to search and filter the product catalogue so that I can quickly find the products I need instead of scrolling the whole list. +ac=The products endpoint accepts query parameters for a text search (name/SKU) and at least one filter (e.g. supplier or price range). +ac=The products page has a search box and filter control wired to the API. +ac=Results update to reflect the active search/filter, and clearing them restores the full list. +ac=Search is case-insensitive and handles "no results" cleanly. +--- +title=Inventory management / stock levels +priority=High +size=L +layers=frontend, API, database +body=As a branch manager, I want each product to track a stock level so that customers can't order more than is available and staff can see what needs restocking. +ac=Products carry a stock/quantity-on-hand value (schema/migration added following existing patterns). +ac=The API returns stock levels and prevents ordering more than the available quantity. +ac=The frontend shows stock status (e.g. in stock / low / out of stock) on product views. +ac=Placing an order decrements the relevant stock, and the change is covered by tests. +--- +title=Supplier & branch management view +priority=Medium +size=M +layers=frontend, API +body=As an admin, I want to view and manage suppliers and branches so that I can keep the organisation's supply-chain data accurate, mirroring the existing product admin experience. +ac=An admin view lists suppliers (and/or branches) using the existing admin UI patterns. +ac=An admin can create or edit a supplier/branch through the API. +ac=Basic validation is applied (e.g. required name) with consistent error responses. +ac=The view is reachable from the existing admin navigation. +--- +title=Delivery tracking & status +priority=Medium +size=M +layers=frontend, API +body=As a branch manager, I want to see deliveries and their status so that I can track incoming stock from suppliers against the orders that need it. +ac=The API exposes deliveries with their status and links to the related order details. +ac=The frontend shows a deliveries list with supplier, date, and status. +ac=A delivery's status can be advanced (e.g. pending → shipped → delivered) via the API. +ac=Status changes are validated and reflected in the UI. +--- +title=Order status workflow +priority=Medium +size=S–M +layers=frontend, API +body=As a branch manager, I want to move an order through a clear set of statuses so that everyone can see where each order stands from placement to fulfilment. +ac=Orders support a defined set of statuses (e.g. pending → confirmed → shipped → delivered / cancelled). +ac=The API only allows valid status transitions and rejects invalid ones with a clear error. +ac=The frontend shows the current status and allows an authorised user to advance it. +ac=The transition rules are covered by unit tests. +--- +title=Cart quantity validation +priority=Low +size=S +layers=frontend, API +body=As a customer, I want the cart to reject nonsensical quantities so that I can't accidentally order zero, a negative amount, or a non-numeric quantity. +ac=Quantities must be positive integers; invalid values are rejected on both the API and the UI. +ac=The API returns a consistent validation error for bad quantities. +ac=The cart UI shows an inline message and blocks the update/checkout until it's fixed. +ac=The validation logic has unit-test coverage. +ITEMS_EOF + +# --------------------------------------------------------------------------- +# Ensure labels exist +# --------------------------------------------------------------------------- +echo "==> Ensuring labels exist..." +LABELS_ENSURED=0 +for entry in "${LABELS[@]}"; do + IFS='|' read -r name color desc <<<"$entry" + if $DRY_RUN; then + echo " [dry-run] would ensure label: $name" + LABELS_ENSURED=$((LABELS_ENSURED + 1)) + continue + fi + # 'gh label create --force' creates or updates, so it tolerates existing labels. + if gh label create "$name" --color "$color" --description "$desc" --force "${REPO_ARGS[@]}" >/dev/null 2>&1; then + echo " ensured label: $name" + LABELS_ENSURED=$((LABELS_ENSURED + 1)) + else + echo " warning: could not ensure label '$name' (continuing)" >&2 + fi +done + +# --------------------------------------------------------------------------- +# Fetch existing open issue titles once (for duplicate detection) +# --------------------------------------------------------------------------- +EXISTING_TITLES="" +if ! $DRY_RUN; then + EXISTING_TITLES="$(gh issue list --state open --limit 200 --json title --jq '.[].title' "${REPO_ARGS[@]}" 2>/dev/null || true)" +fi + +# --------------------------------------------------------------------------- +# Create issues +# --------------------------------------------------------------------------- +CREATED=0 +SKIPPED=0 + +title=""; priority=""; size=""; layers=""; body=""; acs=() + +flush_item() { + [[ -z "$title" ]] && return 0 + + local prio_label="priority:$(echo "$priority" | tr '[:upper:]' '[:lower:]')" + + # Build the issue body (Markdown). + local ac_block="" + local c + for c in "${acs[@]}"; do + ac_block+="- [ ] $c"$'\n' + done + + local issue_body + issue_body="$body"$'\n\n' + issue_body+="**Priority:** $priority | **Size:** $size"$'\n\n' + issue_body+="**Affected layers:** $layers"$'\n\n' + issue_body+="**Acceptance criteria**"$'\n\n' + issue_body+="$ac_block"$'\n' + issue_body+="_Seeded from assets/backlog.md for Challenge 3._" + + # Duplicate check (open issues with identical title). + if [[ -n "$EXISTING_TITLES" ]] && grep -Fxq "$title" <<<"$EXISTING_TITLES"; then + echo " skip (already exists): $title" + SKIPPED=$((SKIPPED + 1)) + return 0 + fi + + if $DRY_RUN; then + echo " [dry-run] would create issue: $title [backlog, $prio_label]" + CREATED=$((CREATED + 1)) + return 0 + fi + + if gh issue create \ + --title "$title" \ + --body "$issue_body" \ + --label "backlog" \ + --label "$prio_label" \ + "${REPO_ARGS[@]}" >/dev/null 2>&1; then + echo " created: $title" + CREATED=$((CREATED + 1)) + else + echo " warning: failed to create issue '$title' (continuing)" >&2 + SKIPPED=$((SKIPPED + 1)) + fi +} + +echo "==> Seeding backlog issues..." +while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + flush_item + title=""; priority=""; size=""; layers=""; body=""; acs=() + continue + fi + key="${line%%=*}" + val="${line#*=}" + case "$key" in + title) title="$val" ;; + priority) priority="$val" ;; + size) size="$val" ;; + layers) layers="$val" ;; + body) body="$val" ;; + ac) acs+=("$val") ;; + esac +done <<<"$ITEMS" +flush_item # last block (no trailing separator) + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +TARGET="${REPO:-current repo}" +echo "" +echo "==> Summary ($TARGET)" +if $DRY_RUN; then + echo " Mode: DRY RUN (no changes made)" +fi +echo " Labels ensured: $LABELS_ENSURED" +echo " Issues created: $CREATED" +echo " Issues skipped: $SKIPPED" +echo "Done." diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/Makefile b/03-Azure/01-04-AI/01_Agentic_SDLC/src/Makefile new file mode 100644 index 000000000..8a613e99e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/Makefile @@ -0,0 +1,287 @@ +# OctoCAT Supply Chain Management - Makefile +# Cross-platform compatible (Linux, macOS, Windows with Git Bash or WSL) + +# Detect OS for platform-specific commands +ifeq ($(OS),Windows_NT) + DETECTED_OS := Windows + RM_RF = powershell -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" + MKDIR_P = powershell -Command "New-Item -ItemType Directory -Force -Path" + PYTHON_CMD = python + PYTHON_API_EXEC = $(PYTHON_CMD) +else + DETECTED_OS := $(shell uname -s) + RM_RF = rm -rf + MKDIR_P = mkdir -p + PYTHON_CMD ?= python3 + PYTHON_API_EXEC = ./.venv/bin/python +endif + +API_TS_DIR := api-ts +API_CS_DIR := api-cs +API_CS_TEST_DIR := api-cs-tests +API_PY_DIR := api-py +API_JAVA_DIR := api-java +FRONTEND_DIR := frontend +PLAYWRIGHT_BROWSERS_PATH ?= $(HOME)/.cache/ms-playwright + +# Resolve backend URL for frontend dev server. +# In Codespaces, browsers cannot reach container-local localhost, so use the +# forwarded host pattern: https://-.. +CODESPACES_DOMAIN := $(if $(GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),$(GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),app.github.dev) +define resolve_api_url +$(if $(strip $(CODESPACE_NAME)),https://$(CODESPACE_NAME)-$(1).$(CODESPACES_DOMAIN),http://localhost:$(1)) +endef + +TS_API_URL := $(call resolve_api_url,3000) +CS_API_URL := $(call resolve_api_url,3000) +PY_API_URL := $(call resolve_api_url,3000) +JAVA_API_URL := $(call resolve_api_url,3000) + +# Default API target (TypeScript) +API_DIR := $(API_TS_DIR) + +.DEFAULT_GOAL := help + +##@ General + +.PHONY: help +help: ## Display this help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.PHONY: info +info: ## Show detected OS and directories + @echo "Detected OS: $(DETECTED_OS)" + @echo "Frontend Directory: $(FRONTEND_DIR)" + @echo "API variants: $(API_TS_DIR) $(API_CS_DIR) $(API_PY_DIR) $(API_JAVA_DIR)" + +##@ Installation + +.PHONY: install +install: install-ts install-frontend ## Install all dependencies (all variants) + +.PHONY: install-ts +install-ts: ## Install TypeScript API dependencies + cd $(API_TS_DIR) && npm install + +.PHONY: install-cs +install-cs: ## Restore C# API NuGet packages + cd $(API_CS_DIR) && dotnet restore + cd $(API_CS_TEST_DIR) && dotnet restore + +.PHONY: install-py +install-py: ## Install Python API dependencies +ifeq ($(OS),Windows_NT) + cd $(API_PY_DIR) && $(PYTHON_CMD) -m pip install -r requirements.txt +else + cd $(API_PY_DIR) && $(PYTHON_CMD) -m venv .venv && $(PYTHON_API_EXEC) -m pip install -r requirements.txt +endif + +.PHONY: install-java +install-java: ## Download Java API Maven dependencies + cd $(API_JAVA_DIR) && mvn dependency:resolve -q + +.PHONY: install-frontend +install-frontend: ## Install frontend dependencies + cd $(FRONTEND_DIR) && npm install + +##@ Development — Full Stack (API + Frontend) + +.PHONY: dev +dev: dev-ts ## Start TypeScript API + frontend (default) + +.PHONY: dev-ts +dev-ts: ## Start TypeScript API (port 3000) + frontend +ifeq ($(OS),Windows_NT) + npx concurrently --kill-others "cd $(API_TS_DIR) && npm run dev" "cd $(FRONTEND_DIR) && set VITE_API_URL=$(TS_API_URL) && npm run dev" +else + @trap 'kill 0' INT; \ + (cd $(API_TS_DIR) && npm run dev) & \ + (cd $(FRONTEND_DIR) && VITE_API_URL=$(TS_API_URL) npm run dev) & \ + wait +endif + +.PHONY: dev-cs +dev-cs: ## Start C# API (port 3000) + frontend +ifeq ($(OS),Windows_NT) + npx concurrently --kill-others "cd $(API_CS_DIR) && set PORT=3000 && dotnet run" "cd $(FRONTEND_DIR) && set VITE_API_URL=$(CS_API_URL) && npm run dev" +else + @trap 'kill 0' INT; \ + (cd $(API_CS_DIR) && PORT=3000 dotnet run) & \ + (cd $(FRONTEND_DIR) && VITE_API_URL=$(CS_API_URL) npm run dev) & \ + wait +endif + +.PHONY: dev-py +dev-py: ## Start Python API (port 3000) + frontend +ifeq ($(OS),Windows_NT) + npx concurrently --kill-others "cd $(API_PY_DIR) && $(PYTHON_API_EXEC) -m uvicorn main:app --reload --port 3000" "cd $(FRONTEND_DIR) && set VITE_API_URL=$(PY_API_URL) && npm run dev" +else + @trap 'kill 0' INT; \ + (cd $(API_PY_DIR) && $(PYTHON_API_EXEC) -m uvicorn main:app --reload --port 3000 --host 0.0.0.0) & \ + (cd $(FRONTEND_DIR) && VITE_API_URL=$(PY_API_URL) npm run dev) & \ + wait +endif + +.PHONY: dev-java +dev-java: ## Start Java API (port 3000) + frontend +ifeq ($(OS),Windows_NT) + npx concurrently --kill-others "cd $(API_JAVA_DIR) && mvn spring-boot:run" "cd $(FRONTEND_DIR) && set VITE_API_URL=$(JAVA_API_URL) && npm run dev" +else + @trap 'kill 0' INT; \ + (cd $(API_JAVA_DIR) && mvn spring-boot:run) & \ + (cd $(FRONTEND_DIR) && VITE_API_URL=$(JAVA_API_URL) npm run dev) & \ + wait +endif + +##@ Development — API Only + +.PHONY: dev-api-ts +dev-api-ts: ## Start only the TypeScript API (port 3000) + cd $(API_TS_DIR) && npm run dev + +.PHONY: dev-api-cs +dev-api-cs: ## Start only the C# API (port 3000) + cd $(API_CS_DIR) && PORT=3000 dotnet run + +.PHONY: dev-api-py +dev-api-py: ## Start only the Python API (port 3000) + cd $(API_PY_DIR) && $(PYTHON_API_EXEC) -m uvicorn main:app --reload --port 3000 --host 0.0.0.0 + +.PHONY: dev-api-java +dev-api-java: ## Start only the Java API (port 3000) + cd $(API_JAVA_DIR) && mvn spring-boot:run + +.PHONY: dev-frontend +dev-frontend: ## Start only the frontend (points to TypeScript API by default) + cd $(FRONTEND_DIR) && npm run dev + +##@ Database + +.PHONY: db-init +db-init: ## Initialize database schema (TypeScript runner) + cd $(API_TS_DIR) && npm run db:init:dev + +.PHONY: db-seed +db-seed: ## Initialize and seed database with sample data (TypeScript runner) + cd $(API_TS_DIR) && npm run db:seed:dev + +##@ Building + +.PHONY: build +build: build-ts build-frontend ## Build TypeScript API + frontend + +.PHONY: build-ts +build-ts: ## Build the TypeScript API + cd $(API_TS_DIR) && npm run build + +.PHONY: build-cs +build-cs: ## Build the C# API + cd $(API_CS_DIR) && dotnet build -c Release + +.PHONY: build-py +build-py: ## Validate Python API (no compile step; checks imports) + cd $(API_PY_DIR) && $(PYTHON_API_EXEC) -m py_compile main.py + +.PHONY: build-java +build-java: ## Build the Java API (Maven) + cd $(API_JAVA_DIR) && mvn package -DskipTests -q + +.PHONY: build-frontend +build-frontend: ## Build the frontend + cd $(FRONTEND_DIR) && npm run build + +##@ Testing + +.PHONY: test +test: test-ts test-frontend ## Run TypeScript API tests + frontend tests + +.PHONY: test-ts +test-ts: ## Run TypeScript API tests + cd $(API_TS_DIR) && npm run test + +.PHONY: test-cs +test-cs: ## Run C# API tests + cd $(API_CS_TEST_DIR) && dotnet test + +.PHONY: test-py +test-py: ## Run Python API tests + cd $(API_PY_DIR) && $(PYTHON_API_EXEC) -m pytest + +.PHONY: test-java +test-java: ## Run Java API tests + cd $(API_JAVA_DIR) && mvn test + +.PHONY: install-playwright-browsers +install-playwright-browsers: ## Install Playwright browser binaries used by frontend e2e tests + @# Skip install if a Chromium binary already exists in the cache + @if find "$(PLAYWRIGHT_BROWSERS_PATH)" -path '*/chrome-linux/chrome' -print -quit 2>/dev/null | grep -q .; then \ + echo "Playwright Chromium already installed, skipping."; \ + else \ + $(MKDIR_P) "$(PLAYWRIGHT_BROWSERS_PATH)"; \ + cd $(FRONTEND_DIR) && PLAYWRIGHT_BROWSERS_PATH="$(PLAYWRIGHT_BROWSERS_PATH)" npx playwright install chromium; \ + fi + +.PHONY: test-frontend +test-frontend: install-playwright-browsers ## Run frontend tests + cd $(FRONTEND_DIR) && PLAYWRIGHT_BROWSERS_PATH="$(PLAYWRIGHT_BROWSERS_PATH)" npm run test + +.PHONY: test-e2e +test-e2e: install-playwright-browsers ## Run end-to-end tests + cd $(FRONTEND_DIR) && PLAYWRIGHT_BROWSERS_PATH="$(PLAYWRIGHT_BROWSERS_PATH)" npm run test:e2e + +.PHONY: test-coverage +test-coverage: ## Run TypeScript API tests with coverage + cd $(API_TS_DIR) && npm run test:coverage + +##@ Linting + +.PHONY: lint +lint: ## Lint TypeScript API + frontend + cd $(API_TS_DIR) && npm run lint + cd $(FRONTEND_DIR) && npm run lint + +.PHONY: lint-fix +lint-fix: ## Lint and auto-fix TypeScript issues + cd $(API_TS_DIR) && npm run lint:fix + cd $(FRONTEND_DIR) && npm run lint -- --fix + +.PHONY: format +format: ## Format TypeScript code with prettier + npx prettier --write "$(API_TS_DIR)/**/*.{ts,tsx}" "$(FRONTEND_DIR)/**/*.{ts,tsx}" + +##@ Code Generation + +.PHONY: swagger +swagger: ## Regenerate Swagger/OpenAPI spec for TypeScript API + cd $(API_TS_DIR) && npm run swagger:generate + +##@ Production + +.PHONY: start +start: ## Start TypeScript API production server + cd $(API_TS_DIR) && npm start + +##@ Docker + +.PHONY: docker-build +docker-build: ## Build Docker images + docker-compose build + +.PHONY: docker-up +docker-up: ## Start Docker containers + docker-compose up + +.PHONY: docker-down +docker-down: ## Stop Docker containers + docker-compose down + +##@ Cleaning + +.PHONY: clean +clean: ## Clean build artifacts and node_modules for all variants + $(RM_RF) $(API_TS_DIR)/node_modules $(API_TS_DIR)/dist $(API_TS_DIR)/data + $(RM_RF) $(API_CS_DIR)/bin $(API_CS_DIR)/obj $(API_CS_DIR)/data + $(RM_RF) $(API_CS_TEST_DIR)/bin $(API_CS_TEST_DIR)/obj + $(RM_RF) $(API_PY_DIR)/__pycache__ $(API_PY_DIR)/.pytest_cache $(API_PY_DIR)/data $(API_PY_DIR)/.venv + $(RM_RF) $(API_JAVA_DIR)/target $(API_JAVA_DIR)/data + $(RM_RF) $(FRONTEND_DIR)/node_modules $(FRONTEND_DIR)/dist diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/README.md new file mode 100644 index 000000000..d4fec9f54 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/README.md @@ -0,0 +1,121 @@ +# OctoSupply Source Code + +This directory contains the OctoSupply application source code, a full-stack supply chain management platform with multiple API language implementations and a shared React frontend. The source represents a production-quality demonstration application for cat tech products and serves as the foundation for this Hackathon. + +## Directory Structure + +### `/api-ts` +Express.js TypeScript backend API server (port **3000**) +- RESTful routes for products, orders, suppliers, branches, deliveries, and inventory +- SQLite database with migrations and seed data +- Comprehensive test coverage with Vitest +- TypeScript type definitions and validation + +### `/api-cs` +ASP.NET Core .NET 10 backend API server (port **3000**) +- Port of the TypeScript API using C# records, repositories, and controllers +- Microsoft.Data.Sqlite with shared migration SQL +- Swashbuckle OpenAPI/Swagger documentation + +### `/api-cs-tests` +C# test project for the .NET API +- xUnit-based tests for repository and API behavior +- Runs via `make test-cs` +- References `/api-cs` as the system under test + +### `/api-py` +FastAPI backend API server (port **3000**) +- Port of the TypeScript API using Pydantic models, async repositories, and APIRouters +- aiosqlite with shared migration SQL +- FastAPI built-in OpenAPI/Swagger documentation + +### `/api-java` +Spring Boot 3 backend API server (port **3000**) +- Port of the TypeScript API using Java records, JdbcTemplate repositories, and @RestControllers +- sqlite-jdbc with shared migration SQL +- SpringDoc OpenAPI documentation + +### `/database` +Shared database schema — used by all API variants +- `migrations/` — SQL migration files applied in order at startup +- `seed/` — SQL seed data files for initial data population + +### `/frontend` +React 18 TypeScript frontend application (port **5137**) +- Vite build tool for fast development and optimized production builds +- Tailwind CSS for styling +- Configurable API URL via `VITE_API_URL` environment variable (targets any API variant) + +### `/docs` +Architecture and development documentation +- System architecture overview +- Database design and SQLite integration +- Build and deployment procedures + +### `Makefile` +Cross-platform build automation supporting all language variants. + +## Quick Start + +```bash +# In the dev container, dependencies are installed during creation. + +# Outside the dev container, install dependencies manually. +make install + +# Start with TypeScript API (default, port 3000) +make dev + +# Start with a specific API variant +make dev-ts # TypeScript API (port 3000) + frontend +make dev-cs # C# API (port 3000) + frontend +make dev-py # Python API (port 3000) + frontend +make dev-java # Java API (port 3000) + frontend + +# Start API only (no frontend) +make dev-api-ts +make dev-api-cs +make dev-api-py +make dev-api-java + +# Install dependencies for a specific variant +make install-ts +make install-cs +make install-py +make install-java + +# Build +make build-ts +make build-cs +make build-py +make build-java + +# Test +make test-ts +make test-cs +make test-py +make test-java +``` + +## API Variants — Port Reference + +| Variant | Port | Framework | Language | +|------------|------|-----------------------|------------| +| TypeScript | 3000 | Express.js | TypeScript | +| C# | 3000 | ASP.NET Core .NET 10 | C# | +| Python | 3000 | FastAPI | Python | +| Java | 3000 | Spring Boot 3 | Java | +| Frontend | 5137 | React 18 + Vite | TypeScript | + +All API variants expose the same REST endpoints and connect to a SQLite database at `./data/app.db` by default. Override with the `DB_FILE` environment variable. + +Swagger/OpenAPI docs are available at `/api-docs` for each running API. + +## Technology Stack + +- **TypeScript API**: Node.js, Express.js, better-sqlite3 +- **C# API**: .NET 10, ASP.NET Core, Microsoft.Data.Sqlite, Swashbuckle +- **Python API**: Python 3.11+, FastAPI, aiosqlite, Pydantic v2 +- **Java API**: Java 21, Spring Boot 3, sqlite-jdbc, SpringDoc OpenAPI +- **Frontend**: React 18, Vite, Tailwind CSS, Playwright (E2E) +- **Database**: SQLite (shared schema in `/database`) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/OctoSupply.Api.Tests.csproj b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/OctoSupply.Api.Tests.csproj new file mode 100644 index 000000000..ad24dd495 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/OctoSupply.Api.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/Repositories/SuppliersRepositoryTests.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/Repositories/SuppliersRepositoryTests.cs new file mode 100644 index 000000000..c96f9fc24 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs-tests/Repositories/SuppliersRepositoryTests.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.Logging.Abstractions; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; +using Xunit; + +namespace OctoSupply.Api.Tests.Repositories; + +public sealed class SuppliersRepositoryTests +{ + [Fact] + public async Task CreateAndFindById_PersistsSupplier() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"octosupply-tests-{Guid.NewGuid():N}.db"); + Environment.SetEnvironmentVariable("DB_FILE", dbPath); + + try + { + var factory = new SqliteConnectionFactory(new NullLogger()); + await EnsureSchemaAsync(factory); + var repository = new SuppliersRepository(factory); + + var created = await repository.CreateAsync(new CreateSupplierRequest + { + Name = "Test Supplier", + Description = "desc", + ContactPerson = "Alice", + Email = "alice@example.com", + Phone = "555-0100", + Active = true, + Verified = false + }); + + var fetched = await repository.FindByIdAsync(created.SupplierId); + + Assert.NotNull(fetched); + Assert.Equal("Test Supplier", fetched!.Name); + Assert.True(fetched.Active); + Assert.False(fetched.Verified); + } + finally + { + Environment.SetEnvironmentVariable("DB_FILE", null); + if (File.Exists(dbPath)) + { + File.Delete(dbPath); + } + } + } + + [Fact] + public async Task Update_WithNoFields_ThrowsValidationException() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"octosupply-tests-{Guid.NewGuid():N}.db"); + Environment.SetEnvironmentVariable("DB_FILE", dbPath); + + try + { + var factory = new SqliteConnectionFactory(new NullLogger()); + await EnsureSchemaAsync(factory); + var repository = new SuppliersRepository(factory); + var created = await repository.CreateAsync(new CreateSupplierRequest { Name = "Test Supplier" }); + + var ex = await Assert.ThrowsAsync(() => repository.UpdateAsync(created.SupplierId, new UpdateSupplierRequest())); + Assert.Contains("No fields provided for update", ex.Message, StringComparison.Ordinal); + } + finally + { + Environment.SetEnvironmentVariable("DB_FILE", null); + if (File.Exists(dbPath)) + { + File.Delete(dbPath); + } + } + } + + private static async Task EnsureSchemaAsync(SqliteConnectionFactory connectionFactory) + { + await using var connection = await connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + CREATE TABLE suppliers ( + supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + contact_person TEXT, + email TEXT, + phone TEXT, + active INTEGER NOT NULL DEFAULT 1, + verified INTEGER NOT NULL DEFAULT 0 + ); + """; + await cmd.ExecuteNonQueryAsync(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/BranchesController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/BranchesController.cs new file mode 100644 index 000000000..bc85020c4 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/BranchesController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/branches")] +public sealed class BranchesController(BranchesRepository repository) : ControllerBase +{ + private readonly BranchesRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Branch", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateBranchRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.BranchId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateBranchRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/DeliveriesController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/DeliveriesController.cs new file mode 100644 index 000000000..179626301 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/DeliveriesController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/deliveries")] +public sealed class DeliveriesController(DeliveriesRepository repository) : ControllerBase +{ + private readonly DeliveriesRepository _repository = repository; + + public sealed class UpdateDeliveryStatusRequest + { + public string Status { get; set; } = string.Empty; + public string? DeliveryPartner { get; set; } + } + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Delivery", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateDeliveryRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + if (string.IsNullOrWhiteSpace(request.DeliveryDate)) + { + throw new ValidationException("deliveryDate is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.DeliveryId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateDeliveryRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpPut("{id:int}/status")] + public async Task> UpdateStatus(int id, [FromBody] UpdateDeliveryStatusRequest request) + { + if (string.IsNullOrWhiteSpace(request.Status)) + { + throw new ValidationException("status is required"); + } + + var updated = await _repository.UpdateStatusAsync(id, request.Status); + if (!string.IsNullOrWhiteSpace(request.DeliveryPartner)) + { + return Ok(new { delivery = updated, commandOutput = string.Empty }); + } + + return Ok(updated); + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/HeadquartersController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/HeadquartersController.cs new file mode 100644 index 000000000..bd2860412 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/HeadquartersController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/headquarters")] +public sealed class HeadquartersController(HeadquartersRepository repository) : ControllerBase +{ + private readonly HeadquartersRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Headquarters", id) : Ok(item); + } + + [HttpGet("{id:int}/metrics")] + public async Task> GetMetrics(int id) => + Ok(await _repository.GetMetricsAsync(id)); + + [HttpGet("{id:int}/label")] + public async Task> GetLabel(int id) + { + var item = await _repository.FindByIdAsync(id); + if (item is null) + { + throw new NotFoundException("Headquarters", id); + } + + var label = $"Location:{item.Name}City:Country:"; + return Ok(new { label }); + } + + [HttpPost] + public async Task> Create([FromBody] CreateHeadquartersRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.HeadquartersId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateHeadquartersRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailDeliveriesController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailDeliveriesController.cs new file mode 100644 index 000000000..bc60df362 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailDeliveriesController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/order-detail-deliveries")] +public sealed class OrderDetailDeliveriesController(OrderDetailDeliveriesRepository repository) : ControllerBase +{ + private readonly OrderDetailDeliveriesRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("OrderDetailDelivery", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateOrderDetailDeliveryRequest request) + { + if (request.OrderDetailId <= 0 || request.DeliveryId <= 0) + { + throw new ValidationException("orderDetailId and deliveryId are required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.OrderDetailDeliveryId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateOrderDetailDeliveryRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailsController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailsController.cs new file mode 100644 index 000000000..79ced50b5 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrderDetailsController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/order-details")] +public sealed class OrderDetailsController(OrderDetailsRepository repository) : ControllerBase +{ + private readonly OrderDetailsRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("OrderDetail", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateOrderDetailRequest request) + { + if (request.OrderId <= 0 || request.ProductId <= 0) + { + throw new ValidationException("orderId and productId are required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.OrderDetailId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateOrderDetailRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrdersController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrdersController.cs new file mode 100644 index 000000000..bb3069cd2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/OrdersController.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/orders")] +public sealed class OrdersController(OrdersRepository repository) : ControllerBase +{ + private readonly OrdersRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Order", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateOrderRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + if (request.BranchId <= 0) + { + throw new ValidationException("branchId is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.OrderId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateOrderRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/ProductsController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/ProductsController.cs new file mode 100644 index 000000000..66d886298 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/ProductsController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/products")] +public sealed class ProductsController(ProductsRepository repository) : ControllerBase +{ + private readonly ProductsRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Product", id) : Ok(item); + } + + [HttpPost] + public async Task> Create([FromBody] CreateProductRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.ProductId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateProductRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/SuppliersController.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/SuppliersController.cs new file mode 100644 index 000000000..c567d9ec2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Controllers/SuppliersController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using OctoSupply.Api.Models; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Controllers; + +[ApiController] +[Route("api/suppliers")] +public sealed class SuppliersController(SuppliersRepository repository) : ControllerBase +{ + private readonly SuppliersRepository _repository = repository; + + [HttpGet] + public async Task>> GetAll() => + Ok(await _repository.FindAllAsync()); + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var item = await _repository.FindByIdAsync(id); + return item is null ? throw new NotFoundException("Supplier", id) : Ok(item); + } + + [HttpGet("{id:int}/status")] + public async Task> GetStatus(int id) + { + var supplier = await _repository.FindByIdAsync(id); + if (supplier is null) + { + throw new NotFoundException("Supplier", id); + } + + // Mirrors TypeScript behavior where status currently always resolves to APPROVED. + return Ok(new { status = "APPROVED" }); + } + + [HttpPost] + public async Task> Create([FromBody] CreateSupplierRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ValidationException("name is required"); + } + + var created = await _repository.CreateAsync(request); + return CreatedAtAction(nameof(GetById), new { id = created.SupplierId }, created); + } + + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] UpdateSupplierRequest request) => + Ok(await _repository.UpdateAsync(id, request)); + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/DatabaseMigrator.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/DatabaseMigrator.cs new file mode 100644 index 000000000..bfcac519e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/DatabaseMigrator.cs @@ -0,0 +1,159 @@ +using Microsoft.Data.Sqlite; + +namespace OctoSupply.Api.Data; + +public sealed class DatabaseMigrator( + SqliteConnectionFactory connectionFactory, + ILogger logger) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + private readonly ILogger _logger = logger; + + public async Task InitializeAsync(bool seedOnStartup) + { + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await EnsureMigrationsTableAsync(connection); + await ApplyMigrationsAsync(connection); + if (seedOnStartup) + { + await SeedIfNeededAsync(connection); + } + } + + private static async Task EnsureMigrationsTableAsync(SqliteConnection connection) + { + await using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL, + filename TEXT NOT NULL UNIQUE, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """; + await cmd.ExecuteNonQueryAsync(); + } + + private async Task ApplyMigrationsAsync(SqliteConnection connection) + { + var migrationsDir = ResolvePath("../database/migrations"); + if (!Directory.Exists(migrationsDir)) + { + throw new InvalidOperationException($"Migrations directory not found: {migrationsDir}"); + } + + var migrationFiles = Directory + .GetFiles(migrationsDir, "*.sql") + .OrderBy(Path.GetFileName) + .ToList(); + + foreach (var file in migrationFiles) + { + var filename = Path.GetFileName(file); + if (await IsMigrationAppliedAsync(connection, filename)) + { + continue; + } + + _logger.LogInformation("Applying migration: {Filename}", filename); + var sql = await File.ReadAllTextAsync(file); + await ExecuteSqlStatementsAsync(connection, sql); + + var version = ParseMigrationVersion(filename); + await using var insert = connection.CreateCommand(); + insert.CommandText = "INSERT INTO migrations (version, filename) VALUES ($version, $filename);"; + insert.Parameters.AddWithValue("$version", version); + insert.Parameters.AddWithValue("$filename", filename); + await insert.ExecuteNonQueryAsync(); + } + } + + private async Task SeedIfNeededAsync(SqliteConnection connection) + { + var shouldSeed = true; + try + { + await using var countCmd = connection.CreateCommand(); + countCmd.CommandText = "SELECT COUNT(*) FROM suppliers;"; + var count = Convert.ToInt32(await countCmd.ExecuteScalarAsync()); + shouldSeed = count == 0; + } + catch (SqliteException) + { + shouldSeed = true; + } + catch (InvalidOperationException) + { + shouldSeed = true; + } + + if (!shouldSeed) + { + _logger.LogInformation("Database already seeded; skipping seed files."); + return; + } + + var seedDir = ResolvePath("../database/seed"); + if (!Directory.Exists(seedDir)) + { + _logger.LogInformation("Seed directory not found: {SeedDir}. Skipping seeding.", seedDir); + return; + } + + var seedFiles = Directory + .GetFiles(seedDir, "*.sql") + .OrderBy(Path.GetFileName) + .ToList(); + + foreach (var file in seedFiles) + { + _logger.LogInformation("Applying seed file: {Filename}", Path.GetFileName(file)); + var sql = await File.ReadAllTextAsync(file); + await ExecuteSqlStatementsAsync(connection, sql); + } + } + + private static async Task IsMigrationAppliedAsync(SqliteConnection connection, string filename) + { + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM migrations WHERE filename = $filename;"; + cmd.Parameters.AddWithValue("$filename", filename); + var count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return count > 0; + } + + private static async Task ExecuteSqlStatementsAsync(SqliteConnection connection, string sqlScript) + { + var statements = sqlScript + .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(statement => !string.IsNullOrWhiteSpace(statement)) + .ToList(); + + await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(); + foreach (var statement in statements) + { + await using var cmd = connection.CreateCommand(); + cmd.Transaction = transaction; + cmd.CommandText = statement; + await cmd.ExecuteNonQueryAsync(); + } + await transaction.CommitAsync(); + } + + private static int ParseMigrationVersion(string filename) + { + var prefix = filename.Split('_')[0]; + return int.TryParse(prefix, out var version) ? version : 0; + } + + private static string ResolvePath(string relativePath) + { + var configured = Environment.GetEnvironmentVariable("DB_MIGRATIONS_DIR"); + if (!string.IsNullOrWhiteSpace(configured) && relativePath.Contains("migrations", StringComparison.OrdinalIgnoreCase)) + { + return Path.GetFullPath(configured); + } + + return Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath)); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/SqliteConnectionFactory.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/SqliteConnectionFactory.cs new file mode 100644 index 000000000..26a65bb16 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Data/SqliteConnectionFactory.cs @@ -0,0 +1,38 @@ +using Microsoft.Data.Sqlite; + +namespace OctoSupply.Api.Data; + +public sealed class SqliteConnectionFactory(ILogger logger) +{ + private readonly ILogger _logger = logger; + + public async Task OpenConnectionAsync() + { + var dbFile = Environment.GetEnvironmentVariable("DB_FILE") ?? "./data/app.db"; + if (!string.Equals(dbFile, ":memory:", StringComparison.Ordinal)) + { + var fullPath = Path.GetFullPath(dbFile); + var directory = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + dbFile = fullPath; + } + + var connectionString = $"Data Source={dbFile}"; + var connection = new SqliteConnection(connectionString); + await connection.OpenAsync(); + + await using var pragma = connection.CreateCommand(); + pragma.CommandText = """ + PRAGMA foreign_keys = ON; + PRAGMA journal_mode = WAL; + PRAGMA busy_timeout = 30000; + """; + await pragma.ExecuteNonQueryAsync(); + + _logger.LogDebug("Opened SQLite connection to {DbFile}", dbFile); + return connection; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Branch.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Branch.cs new file mode 100644 index 000000000..02001ad6f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Branch.cs @@ -0,0 +1,33 @@ +namespace OctoSupply.Api.Models; + +public sealed record Branch( + int BranchId, + int HeadquartersId, + string Name, + string? Description, + string? Address, + string? ContactPerson, + string? Email, + string? Phone); + +public sealed class CreateBranchRequest +{ + public int HeadquartersId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? Address { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } +} + +public sealed class UpdateBranchRequest +{ + public int? HeadquartersId { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? Address { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Delivery.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Delivery.cs new file mode 100644 index 000000000..d97efad87 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Delivery.cs @@ -0,0 +1,27 @@ +namespace OctoSupply.Api.Models; + +public sealed record Delivery( + int DeliveryId, + int SupplierId, + string DeliveryDate, + string Name, + string? Description, + string Status); + +public sealed class CreateDeliveryRequest +{ + public int SupplierId { get; set; } + public string DeliveryDate { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string Status { get; set; } = "pending"; +} + +public sealed class UpdateDeliveryRequest +{ + public int? SupplierId { get; set; } + public string? DeliveryDate { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? Status { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Headquarters.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Headquarters.cs new file mode 100644 index 000000000..3c8b00b66 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Headquarters.cs @@ -0,0 +1,30 @@ +namespace OctoSupply.Api.Models; + +public sealed record Headquarters( + int HeadquartersId, + string Name, + string? Description, + string? Address, + string? ContactPerson, + string? Email, + string? Phone); + +public sealed class CreateHeadquartersRequest +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? Address { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } +} + +public sealed class UpdateHeadquartersRequest +{ + public string? Name { get; set; } + public string? Description { get; set; } + public string? Address { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Order.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Order.cs new file mode 100644 index 000000000..bb06c9fa0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Order.cs @@ -0,0 +1,27 @@ +namespace OctoSupply.Api.Models; + +public sealed record Order( + int OrderId, + int BranchId, + string OrderDate, + string Name, + string? Description, + string Status); + +public sealed class CreateOrderRequest +{ + public int BranchId { get; set; } + public string OrderDate { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string Status { get; set; } = "pending"; +} + +public sealed class UpdateOrderRequest +{ + public int? BranchId { get; set; } + public string? OrderDate { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? Status { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetail.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetail.cs new file mode 100644 index 000000000..e00295ee1 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetail.cs @@ -0,0 +1,27 @@ +namespace OctoSupply.Api.Models; + +public sealed record OrderDetail( + int OrderDetailId, + int OrderId, + int ProductId, + int Quantity, + double UnitPrice, + string? Notes); + +public sealed class CreateOrderDetailRequest +{ + public int OrderId { get; set; } + public int ProductId { get; set; } + public int Quantity { get; set; } + public double UnitPrice { get; set; } + public string? Notes { get; set; } +} + +public sealed class UpdateOrderDetailRequest +{ + public int? OrderId { get; set; } + public int? ProductId { get; set; } + public int? Quantity { get; set; } + public double? UnitPrice { get; set; } + public string? Notes { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetailDelivery.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetailDelivery.cs new file mode 100644 index 000000000..ea182346c --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/OrderDetailDelivery.cs @@ -0,0 +1,24 @@ +namespace OctoSupply.Api.Models; + +public sealed record OrderDetailDelivery( + int OrderDetailDeliveryId, + int OrderDetailId, + int DeliveryId, + int Quantity, + string? Notes); + +public sealed class CreateOrderDetailDeliveryRequest +{ + public int OrderDetailId { get; set; } + public int DeliveryId { get; set; } + public int Quantity { get; set; } + public string? Notes { get; set; } +} + +public sealed class UpdateOrderDetailDeliveryRequest +{ + public int? OrderDetailId { get; set; } + public int? DeliveryId { get; set; } + public int? Quantity { get; set; } + public string? Notes { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Product.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Product.cs new file mode 100644 index 000000000..e57b857a7 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Product.cs @@ -0,0 +1,36 @@ +namespace OctoSupply.Api.Models; + +public sealed record Product( + int ProductId, + int SupplierId, + string Name, + string? Description, + double Price, + string Sku, + string Unit, + string? ImgName, + double Discount); + +public sealed class CreateProductRequest +{ + public int SupplierId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public double Price { get; set; } + public string Sku { get; set; } = string.Empty; + public string Unit { get; set; } = string.Empty; + public string? ImgName { get; set; } + public double Discount { get; set; } +} + +public sealed class UpdateProductRequest +{ + public int? SupplierId { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public double? Price { get; set; } + public string? Sku { get; set; } + public string? Unit { get; set; } + public string? ImgName { get; set; } + public double? Discount { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Supplier.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Supplier.cs new file mode 100644 index 000000000..122db2a52 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Models/Supplier.cs @@ -0,0 +1,33 @@ +namespace OctoSupply.Api.Models; + +public sealed record Supplier( + int SupplierId, + string Name, + string? Description, + string? ContactPerson, + string? Email, + string? Phone, + bool Active, + bool Verified); + +public sealed class CreateSupplierRequest +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public bool Active { get; set; } = true; + public bool Verified { get; set; } = false; +} + +public sealed class UpdateSupplierRequest +{ + public string? Name { get; set; } + public string? Description { get; set; } + public string? ContactPerson { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public bool? Active { get; set; } + public bool? Verified { get; set; } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/OctoSupply.Api.csproj b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/OctoSupply.Api.csproj new file mode 100644 index 000000000..0578db242 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/OctoSupply.Api.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + OctoSupply.Api + OctoSupply.Api + + + + + + + + diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Program.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Program.cs new file mode 100644 index 000000000..807efcf05 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Program.cs @@ -0,0 +1,62 @@ +using OctoSupply.Api.Data; +using OctoSupply.Api.Repositories; +using OctoSupply.Api.Utils; + +var builder = WebApplication.CreateBuilder(args); + +var port = Environment.GetEnvironmentVariable("PORT") ?? "3000"; +builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApi(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("ApiCors", policy => + { + var configuredOrigins = Environment.GetEnvironmentVariable("API_CORS_ORIGINS"); + if (!string.IsNullOrWhiteSpace(configuredOrigins)) + { + var origins = configuredOrigins + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + policy.WithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials(); + return; + } + + policy + .SetIsOriginAllowed(origin => + origin.StartsWith("http://localhost:", StringComparison.OrdinalIgnoreCase) || + origin.StartsWith("http://127.0.0.1:", StringComparison.OrdinalIgnoreCase) || + origin.Contains(".app.github.dev", StringComparison.OrdinalIgnoreCase) || + origin.Contains(".azurecontainerapps.io", StringComparison.OrdinalIgnoreCase)) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + +var app = builder.Build(); + +app.MapOpenApi("/api-docs.json"); +app.UseCors("ApiCors"); +app.UseMiddleware(); + +app.MapGet("/", () => "Hello, world!"); +app.MapControllers(); + +var migrator = app.Services.GetRequiredService(); +await migrator.InitializeAsync(seedOnStartup: true); + +app.Run(); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Properties/launchSettings.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Properties/launchSettings.json new file mode 100644 index 000000000..c8d527fea --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5036", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7096;http://localhost:5036", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/README.md new file mode 100644 index 000000000..1ac73573f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/README.md @@ -0,0 +1,40 @@ +# OctoSupply API (C#) + +ASP.NET Core (.NET 10) port of the OctoSupply API. + +## Run + +```bash +dotnet restore +dotnet run +``` + +Default API URL: `http://localhost:3000` + +## Environment + +- `PORT` (default: `3000`) +- `DB_FILE` (default: `./data/app.db`) +- `API_CORS_ORIGINS` (comma-separated override; optional) + +## Database + +On startup, the API applies SQL migrations from: + +- `../database/migrations/*.sql` + +and seeds from: + +- `../database/seed/*.sql` + +## OpenAPI + +- OpenAPI JSON: `http://localhost:3000/api-docs.json` + +## Tests + +Run C# tests from the `src/` directory: + +```bash +make test-cs +``` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/BranchesRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/BranchesRepository.cs new file mode 100644 index 000000000..b915a11c9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/BranchesRepository.cs @@ -0,0 +1,121 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class BranchesRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM branches ORDER BY branch_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM branches WHERE branch_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateBranchRequest request) + { + const string sql = """ + INSERT INTO branches (headquarters_id, name, description, address, contact_person, email, phone) + VALUES ($headquartersId, $name, $description, $address, $contactPerson, $email, $phone); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$headquartersId", request.HeadquartersId); + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$address", (object?)request.Address ?? DBNull.Value); + cmd.Parameters.AddWithValue("$contactPerson", (object?)request.ContactPerson ?? DBNull.Value); + cmd.Parameters.AddWithValue("$email", (object?)request.Email ?? DBNull.Value); + cmd.Parameters.AddWithValue("$phone", (object?)request.Phone ?? DBNull.Value); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Branch"); + } + + public async Task UpdateAsync(int id, UpdateBranchRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.HeadquartersId.HasValue) { updates.Add("headquarters_id = $headquartersId"); parameters.Add(new SqliteParameter("$headquartersId", request.HeadquartersId.Value)); } + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.Address is not null) { updates.Add("address = $address"); parameters.Add(new SqliteParameter("$address", request.Address)); } + if (request.ContactPerson is not null) { updates.Add("contact_person = $contactPerson"); parameters.Add(new SqliteParameter("$contactPerson", request.ContactPerson)); } + if (request.Email is not null) { updates.Add("email = $email"); parameters.Add(new SqliteParameter("$email", request.Email)); } + if (request.Phone is not null) { updates.Add("phone = $phone"); parameters.Add(new SqliteParameter("$phone", request.Phone)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE branches SET {string.Join(", ", updates)} WHERE branch_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Branch", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Branch"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM branches WHERE branch_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Branch", id); + } + } + + private static Branch Map(SqliteDataReader reader) => + new( + BranchId: reader.GetInt32(reader.GetOrdinal("branch_id")), + HeadquartersId: reader.GetInt32(reader.GetOrdinal("headquarters_id")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + Address: reader["address"] as string, + ContactPerson: reader["contact_person"] as string, + Email: reader["email"] as string, + Phone: reader["phone"] as string + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/DeliveriesRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/DeliveriesRepository.cs new file mode 100644 index 000000000..ed1837cf8 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/DeliveriesRepository.cs @@ -0,0 +1,132 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class DeliveriesRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM deliveries ORDER BY delivery_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM deliveries WHERE delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateDeliveryRequest request) + { + const string sql = """ + INSERT INTO deliveries (supplier_id, delivery_date, name, description, status) + VALUES ($supplierId, $deliveryDate, $name, $description, $status); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$supplierId", request.SupplierId); + cmd.Parameters.AddWithValue("$deliveryDate", request.DeliveryDate); + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$status", request.Status); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Delivery"); + } + + public async Task UpdateAsync(int id, UpdateDeliveryRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.SupplierId.HasValue) { updates.Add("supplier_id = $supplierId"); parameters.Add(new SqliteParameter("$supplierId", request.SupplierId.Value)); } + if (request.DeliveryDate is not null) { updates.Add("delivery_date = $deliveryDate"); parameters.Add(new SqliteParameter("$deliveryDate", request.DeliveryDate)); } + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.Status is not null) { updates.Add("status = $status"); parameters.Add(new SqliteParameter("$status", request.Status)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE deliveries SET {string.Join(", ", updates)} WHERE delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Delivery", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Delivery"); + } + + public async Task UpdateStatusAsync(int id, string status) + { + const string sql = "UPDATE deliveries SET status = $status WHERE delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + cmd.Parameters.AddWithValue("$status", status); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Delivery", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Delivery"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM deliveries WHERE delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Delivery", id); + } + } + + private static Delivery Map(SqliteDataReader reader) => + new( + DeliveryId: reader.GetInt32(reader.GetOrdinal("delivery_id")), + SupplierId: reader.GetInt32(reader.GetOrdinal("supplier_id")), + DeliveryDate: reader.GetString(reader.GetOrdinal("delivery_date")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + Status: reader.GetString(reader.GetOrdinal("status")) + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/HeadquartersRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/HeadquartersRepository.cs new file mode 100644 index 000000000..2fcd6c0fe --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/HeadquartersRepository.cs @@ -0,0 +1,139 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class HeadquartersRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM headquarters ORDER BY headquarters_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM headquarters WHERE headquarters_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task GetMetricsAsync(int id) + { + var headquarters = await FindByIdAsync(id); + if (headquarters is null) + { + throw new NotFoundException("Headquarters", id); + } + + // Mirrors the TypeScript route behavior where missing floor/capacity resolve to 0. + var score = headquarters.HeadquartersId; + var average = headquarters.HeadquartersId / 2.0; + var display = $"HQ-{headquarters.HeadquartersId}0"; + + return new + { + score, + average, + display + }; + } + + public async Task CreateAsync(CreateHeadquartersRequest request) + { + const string sql = """ + INSERT INTO headquarters (name, description, address, contact_person, email, phone) + VALUES ($name, $description, $address, $contactPerson, $email, $phone); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$address", (object?)request.Address ?? DBNull.Value); + cmd.Parameters.AddWithValue("$contactPerson", (object?)request.ContactPerson ?? DBNull.Value); + cmd.Parameters.AddWithValue("$email", (object?)request.Email ?? DBNull.Value); + cmd.Parameters.AddWithValue("$phone", (object?)request.Phone ?? DBNull.Value); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Headquarters"); + } + + public async Task UpdateAsync(int id, UpdateHeadquartersRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.Address is not null) { updates.Add("address = $address"); parameters.Add(new SqliteParameter("$address", request.Address)); } + if (request.ContactPerson is not null) { updates.Add("contact_person = $contactPerson"); parameters.Add(new SqliteParameter("$contactPerson", request.ContactPerson)); } + if (request.Email is not null) { updates.Add("email = $email"); parameters.Add(new SqliteParameter("$email", request.Email)); } + if (request.Phone is not null) { updates.Add("phone = $phone"); parameters.Add(new SqliteParameter("$phone", request.Phone)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE headquarters SET {string.Join(", ", updates)} WHERE headquarters_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Headquarters", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Headquarters"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM headquarters WHERE headquarters_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Headquarters", id); + } + } + + private static Headquarters Map(SqliteDataReader reader) => + new( + HeadquartersId: reader.GetInt32(reader.GetOrdinal("headquarters_id")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + Address: reader["address"] as string, + ContactPerson: reader["contact_person"] as string, + Email: reader["email"] as string, + Phone: reader["phone"] as string + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailDeliveriesRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailDeliveriesRepository.cs new file mode 100644 index 000000000..8c592f5fe --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailDeliveriesRepository.cs @@ -0,0 +1,112 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class OrderDetailDeliveriesRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM order_detail_deliveries ORDER BY order_detail_delivery_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM order_detail_deliveries WHERE order_detail_delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateOrderDetailDeliveryRequest request) + { + const string sql = """ + INSERT INTO order_detail_deliveries (order_detail_id, delivery_id, quantity, notes) + VALUES ($orderDetailId, $deliveryId, $quantity, $notes); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$orderDetailId", request.OrderDetailId); + cmd.Parameters.AddWithValue("$deliveryId", request.DeliveryId); + cmd.Parameters.AddWithValue("$quantity", request.Quantity); + cmd.Parameters.AddWithValue("$notes", (object?)request.Notes ?? DBNull.Value); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created OrderDetailDelivery"); + } + + public async Task UpdateAsync(int id, UpdateOrderDetailDeliveryRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.OrderDetailId.HasValue) { updates.Add("order_detail_id = $orderDetailId"); parameters.Add(new SqliteParameter("$orderDetailId", request.OrderDetailId.Value)); } + if (request.DeliveryId.HasValue) { updates.Add("delivery_id = $deliveryId"); parameters.Add(new SqliteParameter("$deliveryId", request.DeliveryId.Value)); } + if (request.Quantity.HasValue) { updates.Add("quantity = $quantity"); parameters.Add(new SqliteParameter("$quantity", request.Quantity.Value)); } + if (request.Notes is not null) { updates.Add("notes = $notes"); parameters.Add(new SqliteParameter("$notes", request.Notes)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE order_detail_deliveries SET {string.Join(", ", updates)} WHERE order_detail_delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("OrderDetailDelivery", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated OrderDetailDelivery"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM order_detail_deliveries WHERE order_detail_delivery_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("OrderDetailDelivery", id); + } + } + + private static OrderDetailDelivery Map(SqliteDataReader reader) => + new( + OrderDetailDeliveryId: reader.GetInt32(reader.GetOrdinal("order_detail_delivery_id")), + OrderDetailId: reader.GetInt32(reader.GetOrdinal("order_detail_id")), + DeliveryId: reader.GetInt32(reader.GetOrdinal("delivery_id")), + Quantity: reader.GetInt32(reader.GetOrdinal("quantity")), + Notes: reader["notes"] as string + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailsRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailsRepository.cs new file mode 100644 index 000000000..276def5f6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrderDetailsRepository.cs @@ -0,0 +1,115 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class OrderDetailsRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM order_details ORDER BY order_detail_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM order_details WHERE order_detail_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateOrderDetailRequest request) + { + const string sql = """ + INSERT INTO order_details (order_id, product_id, quantity, unit_price, notes) + VALUES ($orderId, $productId, $quantity, $unitPrice, $notes); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$orderId", request.OrderId); + cmd.Parameters.AddWithValue("$productId", request.ProductId); + cmd.Parameters.AddWithValue("$quantity", request.Quantity); + cmd.Parameters.AddWithValue("$unitPrice", request.UnitPrice); + cmd.Parameters.AddWithValue("$notes", (object?)request.Notes ?? DBNull.Value); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created OrderDetail"); + } + + public async Task UpdateAsync(int id, UpdateOrderDetailRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.OrderId.HasValue) { updates.Add("order_id = $orderId"); parameters.Add(new SqliteParameter("$orderId", request.OrderId.Value)); } + if (request.ProductId.HasValue) { updates.Add("product_id = $productId"); parameters.Add(new SqliteParameter("$productId", request.ProductId.Value)); } + if (request.Quantity.HasValue) { updates.Add("quantity = $quantity"); parameters.Add(new SqliteParameter("$quantity", request.Quantity.Value)); } + if (request.UnitPrice.HasValue) { updates.Add("unit_price = $unitPrice"); parameters.Add(new SqliteParameter("$unitPrice", request.UnitPrice.Value)); } + if (request.Notes is not null) { updates.Add("notes = $notes"); parameters.Add(new SqliteParameter("$notes", request.Notes)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE order_details SET {string.Join(", ", updates)} WHERE order_detail_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("OrderDetail", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated OrderDetail"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM order_details WHERE order_detail_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("OrderDetail", id); + } + } + + private static OrderDetail Map(SqliteDataReader reader) => + new( + OrderDetailId: reader.GetInt32(reader.GetOrdinal("order_detail_id")), + OrderId: reader.GetInt32(reader.GetOrdinal("order_id")), + ProductId: reader.GetInt32(reader.GetOrdinal("product_id")), + Quantity: reader.GetInt32(reader.GetOrdinal("quantity")), + UnitPrice: reader.GetDouble(reader.GetOrdinal("unit_price")), + Notes: reader["notes"] as string + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrdersRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrdersRepository.cs new file mode 100644 index 000000000..846c4f4a1 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/OrdersRepository.cs @@ -0,0 +1,115 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class OrdersRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM orders ORDER BY order_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM orders WHERE order_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateOrderRequest request) + { + const string sql = """ + INSERT INTO orders (branch_id, order_date, name, description, status) + VALUES ($branchId, $orderDate, $name, $description, $status); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$branchId", request.BranchId); + cmd.Parameters.AddWithValue("$orderDate", request.OrderDate); + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$status", request.Status); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Order"); + } + + public async Task UpdateAsync(int id, UpdateOrderRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.BranchId.HasValue) { updates.Add("branch_id = $branchId"); parameters.Add(new SqliteParameter("$branchId", request.BranchId.Value)); } + if (request.OrderDate is not null) { updates.Add("order_date = $orderDate"); parameters.Add(new SqliteParameter("$orderDate", request.OrderDate)); } + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.Status is not null) { updates.Add("status = $status"); parameters.Add(new SqliteParameter("$status", request.Status)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE orders SET {string.Join(", ", updates)} WHERE order_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Order", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Order"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM orders WHERE order_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Order", id); + } + } + + private static Order Map(SqliteDataReader reader) => + new( + OrderId: reader.GetInt32(reader.GetOrdinal("order_id")), + BranchId: reader.GetInt32(reader.GetOrdinal("branch_id")), + OrderDate: reader.GetString(reader.GetOrdinal("order_date")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + Status: reader.GetString(reader.GetOrdinal("status")) + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/ProductsRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/ProductsRepository.cs new file mode 100644 index 000000000..fc1b795d2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/ProductsRepository.cs @@ -0,0 +1,124 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class ProductsRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM products ORDER BY product_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM products WHERE product_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateProductRequest request) + { + const string sql = """ + INSERT INTO products (supplier_id, name, description, price, sku, unit, img_name, discount) + VALUES ($supplierId, $name, $description, $price, $sku, $unit, $imgName, $discount); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$supplierId", request.SupplierId); + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$price", request.Price); + cmd.Parameters.AddWithValue("$sku", request.Sku); + cmd.Parameters.AddWithValue("$unit", request.Unit); + cmd.Parameters.AddWithValue("$imgName", (object?)request.ImgName ?? DBNull.Value); + cmd.Parameters.AddWithValue("$discount", request.Discount); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Product"); + } + + public async Task UpdateAsync(int id, UpdateProductRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.SupplierId.HasValue) { updates.Add("supplier_id = $supplierId"); parameters.Add(new SqliteParameter("$supplierId", request.SupplierId.Value)); } + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.Price.HasValue) { updates.Add("price = $price"); parameters.Add(new SqliteParameter("$price", request.Price.Value)); } + if (request.Sku is not null) { updates.Add("sku = $sku"); parameters.Add(new SqliteParameter("$sku", request.Sku)); } + if (request.Unit is not null) { updates.Add("unit = $unit"); parameters.Add(new SqliteParameter("$unit", request.Unit)); } + if (request.ImgName is not null) { updates.Add("img_name = $imgName"); parameters.Add(new SqliteParameter("$imgName", request.ImgName)); } + if (request.Discount.HasValue) { updates.Add("discount = $discount"); parameters.Add(new SqliteParameter("$discount", request.Discount.Value)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE products SET {string.Join(", ", updates)} WHERE product_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Product", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Product"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM products WHERE product_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Product", id); + } + } + + private static Product Map(SqliteDataReader reader) => + new( + ProductId: reader.GetInt32(reader.GetOrdinal("product_id")), + SupplierId: reader.GetInt32(reader.GetOrdinal("supplier_id")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + Price: reader.GetDouble(reader.GetOrdinal("price")), + Sku: reader.GetString(reader.GetOrdinal("sku")), + Unit: reader.GetString(reader.GetOrdinal("unit")), + ImgName: reader["img_name"] as string, + Discount: reader.GetDouble(reader.GetOrdinal("discount")) + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/SuppliersRepository.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/SuppliersRepository.cs new file mode 100644 index 000000000..d583a7a14 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Repositories/SuppliersRepository.cs @@ -0,0 +1,121 @@ +using Microsoft.Data.Sqlite; +using OctoSupply.Api.Data; +using OctoSupply.Api.Models; +using OctoSupply.Api.Utils; + +namespace OctoSupply.Api.Repositories; + +public sealed class SuppliersRepository(SqliteConnectionFactory connectionFactory) +{ + private readonly SqliteConnectionFactory _connectionFactory = connectionFactory; + + public async Task> FindAllAsync() + { + const string sql = "SELECT * FROM suppliers ORDER BY supplier_id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + var items = new List(); + while (await reader.ReadAsync()) + { + items.Add(Map(reader)); + } + return items; + } + + public async Task FindByIdAsync(int id) + { + const string sql = "SELECT * FROM suppliers WHERE supplier_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + await using var reader = await cmd.ExecuteReaderAsync(); + return await reader.ReadAsync() ? Map(reader) : null; + } + + public async Task CreateAsync(CreateSupplierRequest request) + { + const string sql = """ + INSERT INTO suppliers (name, description, contact_person, email, phone, active, verified) + VALUES ($name, $description, $contactPerson, $email, $phone, $active, $verified); + SELECT last_insert_rowid(); + """; + + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$name", request.Name); + cmd.Parameters.AddWithValue("$description", (object?)request.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("$contactPerson", (object?)request.ContactPerson ?? DBNull.Value); + cmd.Parameters.AddWithValue("$email", (object?)request.Email ?? DBNull.Value); + cmd.Parameters.AddWithValue("$phone", (object?)request.Phone ?? DBNull.Value); + cmd.Parameters.AddWithValue("$active", request.Active ? 1 : 0); + cmd.Parameters.AddWithValue("$verified", request.Verified ? 1 : 0); + + var id = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve created Supplier"); + } + + public async Task UpdateAsync(int id, UpdateSupplierRequest request) + { + var updates = new List(); + var parameters = new List(); + if (request.Name is not null) { updates.Add("name = $name"); parameters.Add(new SqliteParameter("$name", request.Name)); } + if (request.Description is not null) { updates.Add("description = $description"); parameters.Add(new SqliteParameter("$description", request.Description)); } + if (request.ContactPerson is not null) { updates.Add("contact_person = $contactPerson"); parameters.Add(new SqliteParameter("$contactPerson", request.ContactPerson)); } + if (request.Email is not null) { updates.Add("email = $email"); parameters.Add(new SqliteParameter("$email", request.Email)); } + if (request.Phone is not null) { updates.Add("phone = $phone"); parameters.Add(new SqliteParameter("$phone", request.Phone)); } + if (request.Active.HasValue) { updates.Add("active = $active"); parameters.Add(new SqliteParameter("$active", request.Active.Value ? 1 : 0)); } + if (request.Verified.HasValue) { updates.Add("verified = $verified"); parameters.Add(new SqliteParameter("$verified", request.Verified.Value ? 1 : 0)); } + + if (updates.Count == 0) + { + throw new ValidationException("No fields provided for update"); + } + + var sql = $"UPDATE suppliers SET {string.Join(", ", updates)} WHERE supplier_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + foreach (var parameter in parameters) + { + cmd.Parameters.Add(parameter); + } + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Supplier", id); + } + + return await FindByIdAsync(id) ?? throw new DatabaseException("Failed to retrieve updated Supplier"); + } + + public async Task DeleteAsync(int id) + { + const string sql = "DELETE FROM suppliers WHERE supplier_id = $id;"; + await using var connection = await _connectionFactory.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$id", id); + var changes = await cmd.ExecuteNonQueryAsync(); + if (changes == 0) + { + throw new NotFoundException("Supplier", id); + } + } + + private static Supplier Map(SqliteDataReader reader) => + new( + SupplierId: reader.GetInt32(reader.GetOrdinal("supplier_id")), + Name: reader.GetString(reader.GetOrdinal("name")), + Description: reader["description"] as string, + ContactPerson: reader["contact_person"] as string, + Email: reader["email"] as string, + Phone: reader["phone"] as string, + Active: Convert.ToInt32(reader["active"]) == 1, + Verified: Convert.ToInt32(reader["verified"]) == 1 + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ApiExceptions.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ApiExceptions.cs new file mode 100644 index 000000000..d10a420c3 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ApiExceptions.cs @@ -0,0 +1,46 @@ +namespace OctoSupply.Api.Utils; + +public class ApiException : Exception +{ + public int StatusCode { get; } + public string Code { get; } + + public ApiException(string message, string code, int statusCode) + : base(message) + { + Code = code; + StatusCode = statusCode; + } +} + +public sealed class NotFoundException : ApiException +{ + public NotFoundException(string entity, int id) + : base($"{entity} with ID {id} not found", "NOT_FOUND", StatusCodes.Status404NotFound) + { + } +} + +public sealed class ValidationException : ApiException +{ + public ValidationException(string message) + : base($"Validation error: {message}", "VALIDATION_ERROR", StatusCodes.Status400BadRequest) + { + } +} + +public sealed class ConflictException : ApiException +{ + public ConflictException(string message) + : base($"Conflict: {message}", "CONFLICT", StatusCodes.Status409Conflict) + { + } +} + +public sealed class DatabaseException : ApiException +{ + public DatabaseException(string message) + : base(message, "DATABASE_ERROR", StatusCodes.Status500InternalServerError) + { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ErrorHandlingMiddleware.cs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ErrorHandlingMiddleware.cs new file mode 100644 index 000000000..a1503683a --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/Utils/ErrorHandlingMiddleware.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace OctoSupply.Api.Utils; + +public sealed class ErrorHandlingMiddleware(RequestDelegate next, ILogger logger) +{ + private readonly RequestDelegate _next = next; + private readonly ILogger _logger = logger; + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (ApiException ex) + { + context.Response.StatusCode = ex.StatusCode; + context.Response.ContentType = "application/json"; + var payload = new + { + error = new + { + code = ex.Code, + message = ex.Message + } + }; + await context.Response.WriteAsync(JsonSerializer.Serialize(payload)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled server error"); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = "application/json"; + var payload = new + { + error = new + { + code = "INTERNAL_ERROR", + message = "An unexpected error occurred" + } + }; + await context.Response.WriteAsync(JsonSerializer.Serialize(payload)); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.Development.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.json new file mode 100644 index 000000000..8fd54cadf --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-cs/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Database": { + "FilePath": "./data/app.db" + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/README.md new file mode 100644 index 000000000..108261b9d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/README.md @@ -0,0 +1,33 @@ +# OctoSupply API (Java) + +Spring Boot 3 port of the OctoSupply API. + +## Run + +```bash +mvn spring-boot:run +``` + +Default API URL: `http://localhost:3000` + +## Environment + +- `PORT` (default: `3000`) +- `DB_FILE` (default: `./data/app.db`) +- `API_CORS_ORIGINS` (comma-separated override; optional) +- `DB_MIGRATIONS_DIR` (optional override for migrations directory) + +## Database + +On startup, the API applies SQL migrations from: + +- `../database/migrations/*.sql` + +and seeds from: + +- `../database/seed/*.sql` + +## OpenAPI + +- OpenAPI JSON: `http://localhost:3000/api-docs.json` +- Swagger UI: `http://localhost:3000/api-docs` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/pom.xml b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/pom.xml new file mode 100644 index 000000000..07c63e7e0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.5 + + + + com.octocat + supply + 1.0.0 + octosupply-api-java + OctoSupply API Java port + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.xerial + sqlite-jdbc + 3.47.1.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/Application.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/Application.java new file mode 100644 index 000000000..de80bd8a4 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/Application.java @@ -0,0 +1,11 @@ +package com.octocat.supply; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseConfig.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseConfig.java new file mode 100644 index 000000000..589472a78 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseConfig.java @@ -0,0 +1,37 @@ +package com.octocat.supply.config; + +import org.sqlite.SQLiteConfig; +import org.sqlite.SQLiteDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Configuration +public class DatabaseConfig { + @Bean + public DataSource dataSource(@Value("${app.db-file}") String dbFile) throws Exception { + String resolvedDbFile = dbFile; + if (!":memory:".equals(dbFile)) { + Path dbPath = Paths.get(dbFile).toAbsolutePath().normalize(); + Path parent = dbPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + resolvedDbFile = dbPath.toString(); + } + + SQLiteConfig config = new SQLiteConfig(); + config.enforceForeignKeys(true); + config.setBusyTimeout(30000); + config.setJournalMode(SQLiteConfig.JournalMode.WAL); + + SQLiteDataSource dataSource = new SQLiteDataSource(config); + dataSource.setUrl("jdbc:sqlite:" + resolvedDbFile); + return dataSource; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseInitializer.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseInitializer.java new file mode 100644 index 000000000..35c8e1587 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/DatabaseInitializer.java @@ -0,0 +1,174 @@ +package com.octocat.supply.config; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class DatabaseInitializer implements ApplicationRunner { + private final DataSource dataSource; + + public DatabaseInitializer(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + try (Connection connection = dataSource.getConnection()) { + ensureMigrationsTable(connection); + applyMigrations(connection); + seedIfNeeded(connection); + } + } + + private void ensureMigrationsTable(Connection connection) throws Exception { + try (Statement statement = connection.createStatement()) { + statement.execute(""" + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL, + filename TEXT NOT NULL UNIQUE, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """); + } + } + + private void applyMigrations(Connection connection) throws Exception { + Path migrationsDir = resolvePath("../database/migrations", true); + if (!Files.exists(migrationsDir)) { + throw new IllegalStateException("Migrations directory not found: " + migrationsDir); + } + + List files; + try (var stream = Files.list(migrationsDir)) { + files = stream + .filter(path -> path.getFileName().toString().endsWith(".sql")) + .sorted(Comparator.comparing(path -> path.getFileName().toString())) + .toList(); + } + + for (Path file : files) { + String filename = file.getFileName().toString(); + if (isMigrationApplied(connection, filename)) { + continue; + } + + executeSqlFile(connection, file); + int version = parseMigrationVersion(filename); + try (var statement = connection.prepareStatement("INSERT INTO migrations (version, filename) VALUES (?, ?)")) { + statement.setInt(1, version); + statement.setString(2, filename); + statement.executeUpdate(); + } + } + } + + private boolean isMigrationApplied(Connection connection, String filename) throws Exception { + try (var statement = connection.prepareStatement("SELECT COUNT(*) FROM migrations WHERE filename = ?")) { + statement.setString(1, filename); + try (ResultSet resultSet = statement.executeQuery()) { + return resultSet.next() && resultSet.getInt(1) > 0; + } + } + } + + private void seedIfNeeded(Connection connection) throws Exception { + boolean shouldSeed = true; + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM suppliers")) { + if (resultSet.next()) { + shouldSeed = resultSet.getInt(1) == 0; + } + } catch (Exception ignored) { + shouldSeed = true; + } + + if (!shouldSeed) { + return; + } + + Path seedDir = resolvePath("../database/seed", false); + if (!Files.exists(seedDir)) { + return; + } + + List files; + try (var stream = Files.list(seedDir)) { + files = stream + .filter(path -> path.getFileName().toString().endsWith(".sql")) + .sorted(Comparator.comparing(path -> path.getFileName().toString())) + .toList(); + } + + for (Path file : files) { + executeSqlFile(connection, file); + } + } + + private void executeSqlFile(Connection connection, Path file) throws Exception { + String sql = Files.readString(file); + List statements = splitStatements(sql); + + try (Statement statement = connection.createStatement()) { + connection.setAutoCommit(false); + try { + for (String sqlStatement : statements) { + statement.execute(sqlStatement); + } + connection.commit(); + } catch (Exception ex) { + connection.rollback(); + throw ex; + } finally { + connection.setAutoCommit(true); + } + } + } + + private List splitStatements(String sqlScript) { + String withoutComments = sqlScript.lines() + .filter(line -> !line.trim().startsWith("--")) + .collect(Collectors.joining("\n")); + + List statements = new ArrayList<>(); + for (String part : withoutComments.split(";")) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + statements.add(trimmed); + } + } + return statements; + } + + private int parseMigrationVersion(String filename) { + String prefix = filename.split("_", 2)[0]; + try { + return Integer.parseInt(prefix); + } catch (NumberFormatException ex) { + return 0; + } + } + + private Path resolvePath(String relativePath, boolean allowEnvOverride) { + if (allowEnvOverride) { + String override = System.getenv("DB_MIGRATIONS_DIR"); + if (override != null && !override.isBlank()) { + return Paths.get(override).toAbsolutePath().normalize(); + } + } + return Paths.get(relativePath).toAbsolutePath().normalize(); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/WebConfig.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/WebConfig.java new file mode 100644 index 000000000..01ba099ae --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/config/WebConfig.java @@ -0,0 +1,39 @@ +package com.octocat.supply.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Value("${app.cors-origins:}") + private String configuredOrigins; + + @Override + public void addCorsMappings(CorsRegistry registry) { + var registration = registry.addMapping("/**") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + + if (configuredOrigins != null && !configuredOrigins.isBlank()) { + registration.allowedOrigins(Arrays.stream(configuredOrigins.split(",")) + .map(String::trim) + .filter(origin -> !origin.isEmpty()) + .toArray(String[]::new)); + return; + } + + List defaultPatterns = new ArrayList<>(); + defaultPatterns.add("http://localhost:*"); + defaultPatterns.add("http://127.0.0.1:*"); + defaultPatterns.add("https://*.app.github.dev"); + defaultPatterns.add("https://*.azurecontainerapps.io"); + registration.allowedOriginPatterns(defaultPatterns.toArray(String[]::new)); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/BranchesController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/BranchesController.java new file mode 100644 index 000000000..d78822d2f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/BranchesController.java @@ -0,0 +1,53 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.BranchModel.Branch; +import com.octocat.supply.model.BranchModel.CreateBranchRequest; +import com.octocat.supply.model.BranchModel.UpdateBranchRequest; +import com.octocat.supply.repository.BranchesRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/branches") +public class BranchesController { + private final BranchesRepository repository; + + public BranchesController(BranchesRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Branch getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Branch not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Branch create(@RequestBody CreateBranchRequest request) { + ControllerValidation.requireText(request.name(), "name"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Branch update(@PathVariable int id, @RequestBody UpdateBranchRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Branch not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Branch not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Branch not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ControllerValidation.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ControllerValidation.java new file mode 100644 index 000000000..ef6918fd9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ControllerValidation.java @@ -0,0 +1,20 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.ValidationException; + +final class ControllerValidation { + private ControllerValidation() { + } + + static void requireText(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new ValidationException(fieldName + " is required"); + } + } + + static void requirePositive(Integer value, String fieldName) { + if (value == null || value <= 0) { + throw new ValidationException(fieldName + " is required"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/DeliveriesController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/DeliveriesController.java new file mode 100644 index 000000000..4b1b809ae --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/DeliveriesController.java @@ -0,0 +1,72 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.DeliveryModel.CreateDeliveryRequest; +import com.octocat.supply.model.DeliveryModel.Delivery; +import com.octocat.supply.model.DeliveryModel.UpdateDeliveryRequest; +import com.octocat.supply.model.DeliveryModel.UpdateDeliveryStatusRequest; +import com.octocat.supply.repository.DeliveriesRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/deliveries") +public class DeliveriesController { + private final DeliveriesRepository repository; + + public DeliveriesController(DeliveriesRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Delivery getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Delivery not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Delivery create(@RequestBody CreateDeliveryRequest request) { + ControllerValidation.requireText(request.name(), "name"); + ControllerValidation.requireText(request.deliveryDate(), "deliveryDate"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Delivery update(@PathVariable int id, @RequestBody UpdateDeliveryRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Delivery not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Delivery not found")); + } + + @PutMapping("/{id}/status") + public Object updateStatus(@PathVariable int id, @RequestBody UpdateDeliveryStatusRequest request) { + ControllerValidation.requireText(request.status(), "status"); + repository.findById(id).orElseThrow(() -> new NotFoundException("Delivery not found")); + repository.updateStatus(id, request.status()); + Delivery updated = repository.findById(id).orElseThrow(() -> new NotFoundException("Delivery not found")); + if (request.deliveryPartner() != null && !request.deliveryPartner().isBlank()) { + Map response = new LinkedHashMap<>(); + response.put("delivery", updated); + response.put("commandOutput", ""); + return response; + } + return updated; + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Delivery not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/HeadquartersController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/HeadquartersController.java new file mode 100644 index 000000000..b741a3beb --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/HeadquartersController.java @@ -0,0 +1,67 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.HeadquartersModel.CreateHeadquartersRequest; +import com.octocat.supply.model.HeadquartersModel.Headquarters; +import com.octocat.supply.model.HeadquartersModel.HeadquartersMetrics; +import com.octocat.supply.model.HeadquartersModel.UpdateHeadquartersRequest; +import com.octocat.supply.repository.HeadquartersRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/headquarters") +public class HeadquartersController { + private final HeadquartersRepository repository; + + public HeadquartersController(HeadquartersRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Headquarters getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Headquarters not found")); + } + + @GetMapping("/{id}/metrics") + public HeadquartersMetrics metrics(@PathVariable int id) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Headquarters not found")); + return repository.metrics(id); + } + + @GetMapping("/{id}/label") + public Map label(@PathVariable int id) { + Headquarters headquarters = repository.findById(id).orElseThrow(() -> new NotFoundException("Headquarters not found")); + return Map.of("label", "Location:" + headquarters.name() + "City:Country:"); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Headquarters create(@RequestBody CreateHeadquartersRequest request) { + ControllerValidation.requireText(request.name(), "name"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Headquarters update(@PathVariable int id, @RequestBody UpdateHeadquartersRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Headquarters not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Headquarters not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Headquarters not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailDeliveriesController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailDeliveriesController.java new file mode 100644 index 000000000..2bbfd3f7a --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailDeliveriesController.java @@ -0,0 +1,54 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.OrderDetailDeliveryModel.CreateOrderDetailDeliveryRequest; +import com.octocat.supply.model.OrderDetailDeliveryModel.OrderDetailDelivery; +import com.octocat.supply.model.OrderDetailDeliveryModel.UpdateOrderDetailDeliveryRequest; +import com.octocat.supply.repository.OrderDetailDeliveriesRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/order-detail-deliveries") +public class OrderDetailDeliveriesController { + private final OrderDetailDeliveriesRepository repository; + + public OrderDetailDeliveriesController(OrderDetailDeliveriesRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public OrderDetailDelivery getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetailDelivery not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public OrderDetailDelivery create(@RequestBody CreateOrderDetailDeliveryRequest request) { + ControllerValidation.requirePositive(request.orderDetailId(), "orderDetailId"); + ControllerValidation.requirePositive(request.deliveryId(), "deliveryId"); + return repository.create(request); + } + + @PutMapping("/{id}") + public OrderDetailDelivery update(@PathVariable int id, @RequestBody UpdateOrderDetailDeliveryRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetailDelivery not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetailDelivery not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("OrderDetailDelivery not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailsController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailsController.java new file mode 100644 index 000000000..97e2e4010 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrderDetailsController.java @@ -0,0 +1,54 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.OrderDetailModel.CreateOrderDetailRequest; +import com.octocat.supply.model.OrderDetailModel.OrderDetail; +import com.octocat.supply.model.OrderDetailModel.UpdateOrderDetailRequest; +import com.octocat.supply.repository.OrderDetailsRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/order-details") +public class OrderDetailsController { + private final OrderDetailsRepository repository; + + public OrderDetailsController(OrderDetailsRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public OrderDetail getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetail not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public OrderDetail create(@RequestBody CreateOrderDetailRequest request) { + ControllerValidation.requirePositive(request.orderId(), "orderId"); + ControllerValidation.requirePositive(request.productId(), "productId"); + return repository.create(request); + } + + @PutMapping("/{id}") + public OrderDetail update(@PathVariable int id, @RequestBody UpdateOrderDetailRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetail not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("OrderDetail not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("OrderDetail not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrdersController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrdersController.java new file mode 100644 index 000000000..f96fafad6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/OrdersController.java @@ -0,0 +1,54 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.OrderModel.CreateOrderRequest; +import com.octocat.supply.model.OrderModel.Order; +import com.octocat.supply.model.OrderModel.UpdateOrderRequest; +import com.octocat.supply.repository.OrdersRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/orders") +public class OrdersController { + private final OrdersRepository repository; + + public OrdersController(OrdersRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Order getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Order not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Order create(@RequestBody CreateOrderRequest request) { + ControllerValidation.requireText(request.name(), "name"); + ControllerValidation.requirePositive(request.branchId(), "branchId"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Order update(@PathVariable int id, @RequestBody UpdateOrderRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Order not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Order not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Order not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ProductsController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ProductsController.java new file mode 100644 index 000000000..051661450 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/ProductsController.java @@ -0,0 +1,53 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.ProductModel.CreateProductRequest; +import com.octocat.supply.model.ProductModel.Product; +import com.octocat.supply.model.ProductModel.UpdateProductRequest; +import com.octocat.supply.repository.ProductsRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/products") +public class ProductsController { + private final ProductsRepository repository; + + public ProductsController(ProductsRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Product getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Product not found")); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Product create(@RequestBody CreateProductRequest request) { + ControllerValidation.requireText(request.name(), "name"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Product update(@PathVariable int id, @RequestBody UpdateProductRequest request) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Product not found")); + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Product not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Product not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/RootController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/RootController.java new file mode 100644 index 000000000..66c58ca6e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/RootController.java @@ -0,0 +1,12 @@ +package com.octocat.supply.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class RootController { + @GetMapping("/") + public String root() { + return "Hello, world!"; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/SuppliersController.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/SuppliersController.java new file mode 100644 index 000000000..00b9465ac --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/controller/SuppliersController.java @@ -0,0 +1,62 @@ +package com.octocat.supply.controller; + +import com.octocat.supply.exception.NotFoundException; +import com.octocat.supply.model.SupplierModel.CreateSupplierRequest; +import com.octocat.supply.model.SupplierModel.Supplier; +import com.octocat.supply.model.SupplierModel.UpdateSupplierRequest; +import com.octocat.supply.repository.SuppliersRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/suppliers") +public class SuppliersController { + private final SuppliersRepository repository; + + public SuppliersController(SuppliersRepository repository) { + this.repository = repository; + } + + @GetMapping + public List getAll() { + return repository.findAll(); + } + + @GetMapping("/{id}") + public Supplier getById(@PathVariable int id) { + return repository.findById(id).orElseThrow(() -> new NotFoundException("Supplier not found")); + } + + @GetMapping("/{id}/status") + public Map getStatus(@PathVariable int id) { + repository.findById(id).orElseThrow(() -> new NotFoundException("Supplier not found")); + return Map.of("status", "APPROVED"); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Supplier create(@RequestBody CreateSupplierRequest request) { + ControllerValidation.requireText(request.name(), "name"); + return repository.create(request); + } + + @PutMapping("/{id}") + public Supplier update(@PathVariable int id, @RequestBody UpdateSupplierRequest request) { + if (!repository.findById(id).isPresent()) { + throw new NotFoundException("Supplier not found"); + } + repository.update(id, request); + return repository.findById(id).orElseThrow(() -> new NotFoundException("Supplier not found")); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + if (!repository.delete(id)) { + throw new NotFoundException("Supplier not found"); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ApiException.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ApiException.java new file mode 100644 index 000000000..e58e3c99f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ApiException.java @@ -0,0 +1,14 @@ +package com.octocat.supply.exception; + +public class ApiException extends RuntimeException { + private final String code; + + public ApiException(String code, String message) { + super(message); + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ConflictException.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ConflictException.java new file mode 100644 index 000000000..19c9d9840 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ConflictException.java @@ -0,0 +1,7 @@ +package com.octocat.supply.exception; + +public class ConflictException extends ApiException { + public ConflictException(String message) { + super("CONFLICT_ERROR", message); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/DatabaseException.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/DatabaseException.java new file mode 100644 index 000000000..344412055 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/DatabaseException.java @@ -0,0 +1,7 @@ +package com.octocat.supply.exception; + +public class DatabaseException extends ApiException { + public DatabaseException(String message) { + super("DATABASE_ERROR", message); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/GlobalExceptionHandler.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..c862f1855 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/GlobalExceptionHandler.java @@ -0,0 +1,47 @@ +package com.octocat.supply.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(NotFoundException.class) + public ResponseEntity> handleNotFound(NotFoundException exception) { + return error(HttpStatus.NOT_FOUND, exception); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity> handleValidation(ValidationException exception) { + return error(HttpStatus.BAD_REQUEST, exception); + } + + @ExceptionHandler(ConflictException.class) + public ResponseEntity> handleConflict(ConflictException exception) { + return error(HttpStatus.CONFLICT, exception); + } + + @ExceptionHandler({DatabaseException.class, DataIntegrityViolationException.class}) + public ResponseEntity> handleDatabase(RuntimeException exception) { + ApiException apiException = exception instanceof ApiException api ? api : new DatabaseException("Database operation failed."); + return error(HttpStatus.INTERNAL_SERVER_ERROR, apiException); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneric(Exception exception) { + return error(HttpStatus.INTERNAL_SERVER_ERROR, new ApiException("INTERNAL_ERROR", "Internal server error.")); + } + + private ResponseEntity> error(HttpStatus status, ApiException exception) { + return ResponseEntity.status(status).body(Map.of( + "error", Map.of( + "code", exception.getCode(), + "message", exception.getMessage() + ) + )); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/NotFoundException.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/NotFoundException.java new file mode 100644 index 000000000..a9e46092f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.octocat.supply.exception; + +public class NotFoundException extends ApiException { + public NotFoundException(String message) { + super("NOT_FOUND", message); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ValidationException.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ValidationException.java new file mode 100644 index 000000000..eea60d2f2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/exception/ValidationException.java @@ -0,0 +1,7 @@ +package com.octocat.supply.exception; + +public class ValidationException extends ApiException { + public ValidationException(String message) { + super("VALIDATION_ERROR", message); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/BranchModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/BranchModel.java new file mode 100644 index 000000000..fd5999525 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/BranchModel.java @@ -0,0 +1,40 @@ +package com.octocat.supply.model; + +public final class BranchModel { + private BranchModel() { + } + + public record Branch( + int branchId, + int headquartersId, + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } + + public record CreateBranchRequest( + Integer headquartersId, + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } + + public record UpdateBranchRequest( + Integer headquartersId, + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/DeliveryModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/DeliveryModel.java new file mode 100644 index 000000000..b7faaf442 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/DeliveryModel.java @@ -0,0 +1,40 @@ +package com.octocat.supply.model; + +public final class DeliveryModel { + private DeliveryModel() { + } + + public record Delivery( + int deliveryId, + int orderId, + String deliveryDate, + String name, + String description, + String status + ) { + } + + public record CreateDeliveryRequest( + Integer orderId, + String deliveryDate, + String name, + String description, + String status + ) { + } + + public record UpdateDeliveryRequest( + Integer orderId, + String deliveryDate, + String name, + String description, + String status + ) { + } + + public record UpdateDeliveryStatusRequest( + String status, + String deliveryPartner + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/HeadquartersModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/HeadquartersModel.java new file mode 100644 index 000000000..78d70dc81 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/HeadquartersModel.java @@ -0,0 +1,44 @@ +package com.octocat.supply.model; + +public final class HeadquartersModel { + private HeadquartersModel() { + } + + public record Headquarters( + int headquartersId, + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } + + public record CreateHeadquartersRequest( + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } + + public record UpdateHeadquartersRequest( + String name, + String description, + String address, + String contactPerson, + String email, + String phone + ) { + } + + public record HeadquartersMetrics( + int score, + double average, + String display + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailDeliveryModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailDeliveryModel.java new file mode 100644 index 000000000..1883ee705 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailDeliveryModel.java @@ -0,0 +1,28 @@ +package com.octocat.supply.model; + +public final class OrderDetailDeliveryModel { + private OrderDetailDeliveryModel() { + } + + public record OrderDetailDelivery( + int orderDetailDeliveryId, + int orderDetailId, + int deliveryId, + int quantityDelivered + ) { + } + + public record CreateOrderDetailDeliveryRequest( + Integer orderDetailId, + Integer deliveryId, + Integer quantityDelivered + ) { + } + + public record UpdateOrderDetailDeliveryRequest( + Integer orderDetailId, + Integer deliveryId, + Integer quantityDelivered + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailModel.java new file mode 100644 index 000000000..faceae275 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderDetailModel.java @@ -0,0 +1,28 @@ +package com.octocat.supply.model; + +public final class OrderDetailModel { + private OrderDetailModel() { + } + + public record OrderDetail( + int orderDetailId, + int orderId, + int productId, + int quantity + ) { + } + + public record CreateOrderDetailRequest( + Integer orderId, + Integer productId, + Integer quantity + ) { + } + + public record UpdateOrderDetailRequest( + Integer orderId, + Integer productId, + Integer quantity + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderModel.java new file mode 100644 index 000000000..86cc70fc1 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/OrderModel.java @@ -0,0 +1,34 @@ +package com.octocat.supply.model; + +public final class OrderModel { + private OrderModel() { + } + + public record Order( + int orderId, + int branchId, + String orderDate, + String name, + String description, + String status + ) { + } + + public record CreateOrderRequest( + Integer branchId, + String orderDate, + String name, + String description, + String status + ) { + } + + public record UpdateOrderRequest( + Integer branchId, + String orderDate, + String name, + String description, + String status + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/ProductModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/ProductModel.java new file mode 100644 index 000000000..1a7715647 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/ProductModel.java @@ -0,0 +1,43 @@ +package com.octocat.supply.model; + +public final class ProductModel { + private ProductModel() { + } + + public record Product( + int productId, + int supplierId, + String name, + String description, + double price, + String sku, + String unit, + String imgName, + double discount + ) { + } + + public record CreateProductRequest( + Integer supplierId, + String name, + String description, + Double price, + String sku, + String unit, + String imgName, + Double discount + ) { + } + + public record UpdateProductRequest( + Integer supplierId, + String name, + String description, + Double price, + String sku, + String unit, + String imgName, + Double discount + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/SupplierModel.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/SupplierModel.java new file mode 100644 index 000000000..172792a83 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/model/SupplierModel.java @@ -0,0 +1,40 @@ +package com.octocat.supply.model; + +public final class SupplierModel { + private SupplierModel() { + } + + public record Supplier( + int supplierId, + String name, + String description, + String contactPerson, + String email, + String phone, + boolean active, + boolean verified + ) { + } + + public record CreateSupplierRequest( + String name, + String description, + String contactPerson, + String email, + String phone, + Boolean active, + Boolean verified + ) { + } + + public record UpdateSupplierRequest( + String name, + String description, + String contactPerson, + String email, + String phone, + Boolean active, + Boolean verified + ) { + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/BranchesRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/BranchesRepository.java new file mode 100644 index 000000000..6574e20ea --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/BranchesRepository.java @@ -0,0 +1,79 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.BranchModel.Branch; +import com.octocat.supply.model.BranchModel.CreateBranchRequest; +import com.octocat.supply.model.BranchModel.UpdateBranchRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class BranchesRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Branch( + rs.getInt("branch_id"), + rs.getInt("headquarters_id"), + rs.getString("name"), + rs.getString("description"), + rs.getString("address"), + rs.getString("contact_person"), + rs.getString("email"), + rs.getString("phone") + ); + + public BranchesRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM branches ORDER BY branch_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM branches WHERE branch_id = ?", mapper, id).stream().findFirst(); + } + + public Branch create(CreateBranchRequest request) { + jdbcTemplate.update( + """ + INSERT INTO branches (headquarters_id, name, description, address, contact_person, email, phone) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + request.headquartersId(), + request.name(), + request.description(), + request.address(), + request.contactPerson(), + request.email(), + request.phone() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateBranchRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("headquarters_id", request.headquartersId()); + b.add("name", request.name()); + b.add("description", request.description()); + b.add("address", request.address()); + b.add("contact_person", request.contactPerson()); + b.add("email", request.email()); + b.add("phone", request.phone()); + }, + "branches", + "branch_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM branches WHERE branch_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/DeliveriesRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/DeliveriesRepository.java new file mode 100644 index 000000000..c1db21a76 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/DeliveriesRepository.java @@ -0,0 +1,77 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.DeliveryModel.CreateDeliveryRequest; +import com.octocat.supply.model.DeliveryModel.Delivery; +import com.octocat.supply.model.DeliveryModel.UpdateDeliveryRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class DeliveriesRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Delivery( + rs.getInt("delivery_id"), + rs.getInt("order_id"), + rs.getString("delivery_date"), + rs.getString("name"), + rs.getString("description"), + rs.getString("status") + ); + + public DeliveriesRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM deliveries ORDER BY delivery_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM deliveries WHERE delivery_id = ?", mapper, id).stream().findFirst(); + } + + public Delivery create(CreateDeliveryRequest request) { + jdbcTemplate.update( + """ + INSERT INTO deliveries (order_id, delivery_date, name, description, status) + VALUES (?, ?, ?, ?, ?) + """, + request.orderId(), + request.deliveryDate(), + request.name(), + request.description(), + request.status() == null ? "pending" : request.status() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateDeliveryRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("order_id", request.orderId()); + b.add("delivery_date", request.deliveryDate()); + b.add("name", request.name()); + b.add("description", request.description()); + b.add("status", request.status()); + }, + "deliveries", + "delivery_id", + id, + jdbcTemplate + ); + } + + public boolean updateStatus(int id, String status) { + return jdbcTemplate.update("UPDATE deliveries SET status = ? WHERE delivery_id = ?", status, id) > 0; + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM deliveries WHERE delivery_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/HeadquartersRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/HeadquartersRepository.java new file mode 100644 index 000000000..2dc8ba02c --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/HeadquartersRepository.java @@ -0,0 +1,101 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.HeadquartersModel.CreateHeadquartersRequest; +import com.octocat.supply.model.HeadquartersModel.Headquarters; +import com.octocat.supply.model.HeadquartersModel.HeadquartersMetrics; +import com.octocat.supply.model.HeadquartersModel.UpdateHeadquartersRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Repository +public class HeadquartersRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Headquarters( + rs.getInt("headquarters_id"), + rs.getString("name"), + rs.getString("description"), + rs.getString("address"), + rs.getString("contact_person"), + rs.getString("email"), + rs.getString("phone") + ); + + public HeadquartersRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM headquarters ORDER BY headquarters_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM headquarters WHERE headquarters_id = ?", mapper, id).stream().findFirst(); + } + + public Headquarters create(CreateHeadquartersRequest request) { + jdbcTemplate.update( + """ + INSERT INTO headquarters (name, description, address, contact_person, email, phone) + VALUES (?, ?, ?, ?, ?, ?) + """, + request.name(), + request.description(), + request.address(), + request.contactPerson(), + request.email(), + request.phone() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateHeadquartersRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("name", request.name()); + b.add("description", request.description()); + b.add("address", request.address()); + b.add("contact_person", request.contactPerson()); + b.add("email", request.email()); + b.add("phone", request.phone()); + }, + "headquarters", + "headquarters_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM headquarters WHERE headquarters_id = ?", id) > 0; + } + + public HeadquartersMetrics metrics(int id) { + Map row = jdbcTemplate.queryForMap( + """ + SELECT COUNT(*) AS branch_count, + COUNT(DISTINCT o.order_id) AS order_count + FROM branches b + LEFT JOIN orders o ON o.branch_id = b.branch_id + WHERE b.headquarters_id = ? + """, + id + ); + int branchCount = ((Number) row.get("branch_count")).intValue(); + int orderCount = ((Number) row.get("order_count")).intValue(); + int score = branchCount * 100 + orderCount * 10; + double average = branchCount == 0 ? 0.0 : (double) orderCount / branchCount; + String display = "H-" + id + "-" + score; + return new HeadquartersMetrics(score, average, display); + } + + public String label(int id) { + return "HQ-" + id; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailDeliveriesRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailDeliveriesRepository.java new file mode 100644 index 000000000..499036f67 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailDeliveriesRepository.java @@ -0,0 +1,71 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.OrderDetailDeliveryModel.CreateOrderDetailDeliveryRequest; +import com.octocat.supply.model.OrderDetailDeliveryModel.OrderDetailDelivery; +import com.octocat.supply.model.OrderDetailDeliveryModel.UpdateOrderDetailDeliveryRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class OrderDetailDeliveriesRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new OrderDetailDelivery( + rs.getInt("order_detail_delivery_id"), + rs.getInt("order_detail_id"), + rs.getInt("delivery_id"), + rs.getInt("quantity_delivered") + ); + + public OrderDetailDeliveriesRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM order_detail_deliveries ORDER BY order_detail_delivery_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query( + "SELECT * FROM order_detail_deliveries WHERE order_detail_delivery_id = ?", + mapper, + id + ).stream().findFirst(); + } + + public OrderDetailDelivery create(CreateOrderDetailDeliveryRequest request) { + jdbcTemplate.update( + """ + INSERT INTO order_detail_deliveries (order_detail_id, delivery_id, quantity_delivered) + VALUES (?, ?, ?) + """, + request.orderDetailId(), + request.deliveryId(), + request.quantityDelivered() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateOrderDetailDeliveryRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("order_detail_id", request.orderDetailId()); + b.add("delivery_id", request.deliveryId()); + b.add("quantity_delivered", request.quantityDelivered()); + }, + "order_detail_deliveries", + "order_detail_delivery_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM order_detail_deliveries WHERE order_detail_delivery_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailsRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailsRepository.java new file mode 100644 index 000000000..b93cdbb40 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrderDetailsRepository.java @@ -0,0 +1,67 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.OrderDetailModel.CreateOrderDetailRequest; +import com.octocat.supply.model.OrderDetailModel.OrderDetail; +import com.octocat.supply.model.OrderDetailModel.UpdateOrderDetailRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class OrderDetailsRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new OrderDetail( + rs.getInt("order_detail_id"), + rs.getInt("order_id"), + rs.getInt("product_id"), + rs.getInt("quantity") + ); + + public OrderDetailsRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM order_details ORDER BY order_detail_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM order_details WHERE order_detail_id = ?", mapper, id).stream().findFirst(); + } + + public OrderDetail create(CreateOrderDetailRequest request) { + jdbcTemplate.update( + """ + INSERT INTO order_details (order_id, product_id, quantity) + VALUES (?, ?, ?) + """, + request.orderId(), + request.productId(), + request.quantity() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateOrderDetailRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("order_id", request.orderId()); + b.add("product_id", request.productId()); + b.add("quantity", request.quantity()); + }, + "order_details", + "order_detail_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM order_details WHERE order_detail_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrdersRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrdersRepository.java new file mode 100644 index 000000000..ce990df5d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/OrdersRepository.java @@ -0,0 +1,73 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.OrderModel.CreateOrderRequest; +import com.octocat.supply.model.OrderModel.Order; +import com.octocat.supply.model.OrderModel.UpdateOrderRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class OrdersRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Order( + rs.getInt("order_id"), + rs.getInt("branch_id"), + rs.getString("order_date"), + rs.getString("name"), + rs.getString("description"), + rs.getString("status") + ); + + public OrdersRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM orders ORDER BY order_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM orders WHERE order_id = ?", mapper, id).stream().findFirst(); + } + + public Order create(CreateOrderRequest request) { + jdbcTemplate.update( + """ + INSERT INTO orders (branch_id, order_date, name, description, status) + VALUES (?, ?, ?, ?, ?) + """, + request.branchId(), + request.orderDate(), + request.name(), + request.description(), + request.status() == null ? "pending" : request.status() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateOrderRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("branch_id", request.branchId()); + b.add("order_date", request.orderDate()); + b.add("name", request.name()); + b.add("description", request.description()); + b.add("status", request.status()); + }, + "orders", + "order_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM orders WHERE order_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/ProductsRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/ProductsRepository.java new file mode 100644 index 000000000..8e538a64e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/ProductsRepository.java @@ -0,0 +1,82 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.ProductModel.CreateProductRequest; +import com.octocat.supply.model.ProductModel.Product; +import com.octocat.supply.model.ProductModel.UpdateProductRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class ProductsRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Product( + rs.getInt("product_id"), + rs.getInt("supplier_id"), + rs.getString("name"), + rs.getString("description"), + rs.getDouble("price"), + rs.getString("sku"), + rs.getString("unit"), + rs.getString("img_name"), + rs.getDouble("discount") + ); + + public ProductsRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM products ORDER BY product_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM products WHERE product_id = ?", mapper, id).stream().findFirst(); + } + + public Product create(CreateProductRequest request) { + jdbcTemplate.update( + """ + INSERT INTO products (supplier_id, name, description, price, sku, unit, img_name, discount) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + request.supplierId(), + request.name(), + request.description(), + request.price(), + request.sku(), + request.unit(), + request.imgName(), + request.discount() == null ? 0d : request.discount() + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateProductRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("supplier_id", request.supplierId()); + b.add("name", request.name()); + b.add("description", request.description()); + b.add("price", request.price()); + b.add("sku", request.sku()); + b.add("unit", request.unit()); + b.add("img_name", request.imgName()); + b.add("discount", request.discount()); + }, + "products", + "product_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM products WHERE product_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SqlUpdateBuilder.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SqlUpdateBuilder.java new file mode 100644 index 000000000..039ca8a78 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SqlUpdateBuilder.java @@ -0,0 +1,44 @@ +package com.octocat.supply.repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +final class SqlUpdateBuilder { + private final List sets = new ArrayList<>(); + private final List args = new ArrayList<>(); + + void add(String column, Object value) { + if (value != null) { + sets.add(column + " = ?"); + args.add(value); + } + } + + boolean isEmpty() { + return sets.isEmpty(); + } + + UpdateStatement build(String table, String idColumn, int idValue) { + String sql = "UPDATE " + table + " SET " + String.join(", ", sets) + " WHERE " + idColumn + " = ?"; + List fullArgs = new ArrayList<>(args); + fullArgs.add(idValue); + return new UpdateStatement(sql, fullArgs.toArray()); + } + + record UpdateStatement(String sql, Object[] args) { + int executeWith(org.springframework.jdbc.core.JdbcTemplate jdbcTemplate) { + return jdbcTemplate.update(sql, args); + } + } + + static boolean executeIfPresent(Consumer binder, String table, String idColumn, int idValue, + org.springframework.jdbc.core.JdbcTemplate jdbcTemplate) { + SqlUpdateBuilder builder = new SqlUpdateBuilder(); + binder.accept(builder); + if (builder.isEmpty()) { + return false; + } + return builder.build(table, idColumn, idValue).executeWith(jdbcTemplate) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SuppliersRepository.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SuppliersRepository.java new file mode 100644 index 000000000..deadaaced --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/java/com/octocat/supply/repository/SuppliersRepository.java @@ -0,0 +1,79 @@ +package com.octocat.supply.repository; + +import com.octocat.supply.model.SupplierModel.CreateSupplierRequest; +import com.octocat.supply.model.SupplierModel.Supplier; +import com.octocat.supply.model.SupplierModel.UpdateSupplierRequest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class SuppliersRepository { + private final JdbcTemplate jdbcTemplate; + + private final RowMapper mapper = (rs, rowNum) -> new Supplier( + rs.getInt("supplier_id"), + rs.getString("name"), + rs.getString("description"), + rs.getString("contact_person"), + rs.getString("email"), + rs.getString("phone"), + rs.getInt("active") == 1, + rs.getInt("verified") == 1 + ); + + public SuppliersRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM suppliers ORDER BY supplier_id", mapper); + } + + public Optional findById(int id) { + return jdbcTemplate.query("SELECT * FROM suppliers WHERE supplier_id = ?", mapper, id).stream().findFirst(); + } + + public Supplier create(CreateSupplierRequest request) { + jdbcTemplate.update( + """ + INSERT INTO suppliers (name, description, contact_person, email, phone, active, verified) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + request.name(), + request.description(), + request.contactPerson(), + request.email(), + request.phone(), + request.active() == null || request.active() ? 1 : 0, + request.verified() != null && request.verified() ? 1 : 0 + ); + Integer id = jdbcTemplate.queryForObject("SELECT last_insert_rowid()", Integer.class); + return findById(id).orElseThrow(); + } + + public boolean update(int id, UpdateSupplierRequest request) { + return SqlUpdateBuilder.executeIfPresent( + b -> { + b.add("name", request.name()); + b.add("description", request.description()); + b.add("contact_person", request.contactPerson()); + b.add("email", request.email()); + b.add("phone", request.phone()); + b.add("active", request.active() == null ? null : (request.active() ? 1 : 0)); + b.add("verified", request.verified() == null ? null : (request.verified() ? 1 : 0)); + }, + "suppliers", + "supplier_id", + id, + jdbcTemplate + ); + } + + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM suppliers WHERE supplier_id = ?", id) > 0; + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/resources/application.properties b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/resources/application.properties new file mode 100644 index 000000000..bd5c96142 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/main/resources/application.properties @@ -0,0 +1,6 @@ +server.port=${PORT:3000} +app.db-file=${DB_FILE:./data/app.db} +app.cors-origins=${API_CORS_ORIGINS:} + +springdoc.api-docs.path=/api-docs.json +springdoc.swagger-ui.path=/api-docs diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/ApiEndpointsTest.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/ApiEndpointsTest.java new file mode 100644 index 000000000..3f9b7a164 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/ApiEndpointsTest.java @@ -0,0 +1,58 @@ +package com.octocat.supply; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class ApiEndpointsTest { + @Autowired + private MockMvc mockMvc; + + @Test + void suppliersListReturnsSeededData() throws Exception { + mockMvc.perform(get("/api/suppliers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].supplierId").value(1)) + .andExpect(jsonPath("$[0].name").value("PurrTech Innovations")); + } + + @Test + void supplierStatusEndpointReturnsApproved() throws Exception { + mockMvc.perform(get("/api/suppliers/1/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("APPROVED")); + } + + @Test + void creatingSupplierWithoutNameReturnsValidationError() throws Exception { + mockMvc.perform(post("/api/suppliers") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.error.message").value("name is required")); + } + + @Test + void headquartersMetricsAndLabelAreExposed() throws Exception { + mockMvc.perform(get("/api/headquarters/1/metrics")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.score").isNumber()) + .andExpect(jsonPath("$.average").isNumber()) + .andExpect(jsonPath("$.display").isString()); + + mockMvc.perform(get("/api/headquarters/1/label")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.label").value("Location:CatTech Global HQCity:Country:")); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/RootControllerTest.java b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/RootControllerTest.java new file mode 100644 index 000000000..7002696fa --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/java/com/octocat/supply/RootControllerTest.java @@ -0,0 +1,25 @@ +package com.octocat.supply; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class RootControllerTest { + @Autowired + private MockMvc mockMvc; + + @Test + void rootEndpointReturnsHelloWorld() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().string("Hello, world!")); + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/resources/application.properties b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/resources/application.properties new file mode 100644 index 000000000..97ec0123f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-java/src/test/resources/application.properties @@ -0,0 +1 @@ +app.db-file=./target/test-data/test.db diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/README.md new file mode 100644 index 000000000..805ae45d9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/README.md @@ -0,0 +1,34 @@ +# OctoSupply API (Python) + +FastAPI port of the OctoSupply API. + +## Run + +```bash +pip install -r requirements.txt +uvicorn main:app --reload --port 3000 +``` + +Default API URL: `http://localhost:3000` + +## Environment + +- `PORT` (used by Makefile/uvicorn command, default `3000`) +- `DB_FILE` (default: `./data/app.db`) +- `API_CORS_ORIGINS` (comma-separated override; optional) +- `DB_MIGRATIONS_DIR` (optional override for migrations directory) + +## Database + +On startup, the API applies SQL migrations from: + +- `../database/migrations/*.sql` + +and seeds from: + +- `../database/seed/*.sql` + +## OpenAPI + +- OpenAPI JSON: `http://localhost:3000/openapi.json` +- Swagger UI: `http://localhost:3000/docs` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/__init__.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/__init__.py new file mode 100644 index 000000000..fb24877e3 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/__init__.py @@ -0,0 +1 @@ +"""Database helpers for the Python API.""" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/migrate.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/migrate.py new file mode 100644 index 000000000..c29446172 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/migrate.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import aiosqlite + +from database.sqlite import get_connection + + +def _resolve_path(relative_path: str) -> Path: + configured = os.getenv("DB_MIGRATIONS_DIR") + if configured and "migrations" in relative_path.lower(): + return Path(configured).resolve() + return (Path.cwd() / relative_path).resolve() + + +def _parse_migration_version(filename: str) -> int: + prefix = filename.split("_", 1)[0] + return int(prefix) if prefix.isdigit() else 0 + + +def _split_sql_script(sql_script: str) -> list[str]: + return [statement.strip() for statement in sql_script.split(";") if statement.strip()] + + +async def _execute_sql_statements(connection: aiosqlite.Connection, sql_script: str) -> None: + statements = _split_sql_script(sql_script) + await connection.execute("BEGIN;") + try: + for statement in statements: + await connection.execute(statement) + await connection.commit() + except Exception: + await connection.rollback() + raise + + +async def _ensure_migrations_table(connection: aiosqlite.Connection) -> None: + await connection.execute( + """ + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL, + filename TEXT NOT NULL UNIQUE, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + await connection.commit() + + +async def _is_migration_applied(connection: aiosqlite.Connection, filename: str) -> bool: + cursor = await connection.execute( + "SELECT COUNT(*) FROM migrations WHERE filename = ?;", + (filename,), + ) + row = await cursor.fetchone() + return bool(row and row[0] > 0) + + +async def _apply_migrations(connection: aiosqlite.Connection) -> None: + migrations_dir = _resolve_path("../database/migrations") + if not migrations_dir.exists(): + raise RuntimeError(f"Migrations directory not found: {migrations_dir}") + + migration_files = sorted(migrations_dir.glob("*.sql"), key=lambda f: f.name) + for file_path in migration_files: + if await _is_migration_applied(connection, file_path.name): + continue + + sql = file_path.read_text(encoding="utf-8") + await _execute_sql_statements(connection, sql) + await connection.execute( + "INSERT INTO migrations (version, filename) VALUES (?, ?);", + (_parse_migration_version(file_path.name), file_path.name), + ) + await connection.commit() + + +async def _seed_if_needed(connection: aiosqlite.Connection) -> None: + should_seed = True + try: + cursor = await connection.execute("SELECT COUNT(*) FROM suppliers;") + row = await cursor.fetchone() + should_seed = bool(row and row[0] == 0) + except (aiosqlite.OperationalError, aiosqlite.DatabaseError): + should_seed = True + + if not should_seed: + return + + seed_dir = _resolve_path("../database/seed") + if not seed_dir.exists(): + return + + seed_files = sorted(seed_dir.glob("*.sql"), key=lambda f: f.name) + for file_path in seed_files: + await _execute_sql_statements(connection, file_path.read_text(encoding="utf-8")) + + +async def initialize_database(seed_on_startup: bool) -> None: + async with get_connection() as connection: + await _ensure_migrations_table(connection) + await _apply_migrations(connection) + if seed_on_startup: + await _seed_if_needed(connection) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/sqlite.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/sqlite.py new file mode 100644 index 000000000..d85e97dd7 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/database/sqlite.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import os +from contextlib import asynccontextmanager +from pathlib import Path + +import aiosqlite + + +def _resolve_db_file() -> str: + db_file = os.getenv("DB_FILE", "./data/app.db") + if db_file != ":memory:": + db_path = Path(db_file).resolve() + db_path.parent.mkdir(parents=True, exist_ok=True) + return str(db_path) + return db_file + + +@asynccontextmanager +async def get_connection() -> aiosqlite.Connection: + connection = await aiosqlite.connect(_resolve_db_file()) + connection.row_factory = aiosqlite.Row + await connection.execute("PRAGMA foreign_keys = ON;") + await connection.execute("PRAGMA journal_mode = WAL;") + await connection.execute("PRAGMA busy_timeout = 30000;") + try: + yield connection + finally: + await connection.close() diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/main.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/main.py new file mode 100644 index 000000000..43eb4ce45 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/main.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import os + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from database.migrate import initialize_database +from routes.branches import router as branches_router +from routes.deliveries import router as deliveries_router +from routes.headquarters import router as headquarters_router +from routes.order_detail_deliveries import router as order_detail_deliveries_router +from routes.order_details import router as order_details_router +from routes.orders import router as orders_router +from routes.products import router as products_router +from routes.suppliers import router as suppliers_router +from utils.errors import register_exception_handlers + +app = FastAPI(title="OctoSupply API (Python)", version="1.0.0") + +configured_origins = os.getenv("API_CORS_ORIGINS") +if configured_origins: + allow_origins = [origin.strip() for origin in configured_origins.split(",") if origin.strip()] + allow_origin_regex = None +else: + allow_origins = [ + "http://localhost:5137", + "http://127.0.0.1:5137", + ] + allow_origin_regex = r"^https://.*\.(app\.github\.dev|azurecontainerapps\.io)$" + +app.add_middleware( + CORSMiddleware, + allow_origins=allow_origins, + allow_origin_regex=allow_origin_regex, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +register_exception_handlers(app) + +app.include_router(deliveries_router) +app.include_router(order_detail_deliveries_router) +app.include_router(products_router) +app.include_router(order_details_router) +app.include_router(orders_router) +app.include_router(branches_router) +app.include_router(headquarters_router) +app.include_router(suppliers_router) + + +@app.on_event("startup") +async def startup() -> None: + await initialize_database(seed_on_startup=True) + + +@app.get("/") +async def root() -> str: + return "Hello, world!" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/__init__.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/__init__.py new file mode 100644 index 000000000..208fedbbc --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/__init__.py @@ -0,0 +1 @@ +"""Pydantic API models.""" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/branch.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/branch.py new file mode 100644 index 000000000..f876a1235 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/branch.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Branch(BaseModel): + branchId: int + headquartersId: int + name: str + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + + +class CreateBranchRequest(BaseModel): + headquartersId: int + name: str + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + + +class UpdateBranchRequest(BaseModel): + headquartersId: int | None = None + name: str | None = None + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/delivery.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/delivery.py new file mode 100644 index 000000000..1800c2216 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/delivery.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Delivery(BaseModel): + deliveryId: int + supplierId: int + deliveryDate: str + name: str + description: str | None = None + status: str + + +class CreateDeliveryRequest(BaseModel): + supplierId: int + deliveryDate: str + name: str + description: str | None = None + status: str = "pending" + + +class UpdateDeliveryRequest(BaseModel): + supplierId: int | None = None + deliveryDate: str | None = None + name: str | None = None + description: str | None = None + status: str | None = None + + +class UpdateDeliveryStatusRequest(BaseModel): + status: str + deliveryPartner: str | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/headquarters.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/headquarters.py new file mode 100644 index 000000000..2ac385c56 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/headquarters.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Headquarters(BaseModel): + headquartersId: int + name: str + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + + +class CreateHeadquartersRequest(BaseModel): + name: str + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + + +class UpdateHeadquartersRequest(BaseModel): + name: str | None = None + description: str | None = None + address: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + + +class HeadquartersMetrics(BaseModel): + score: int + average: float + display: str diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order.py new file mode 100644 index 000000000..4d0529bb6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Order(BaseModel): + orderId: int + branchId: int + orderDate: str + name: str + description: str | None = None + status: str + + +class CreateOrderRequest(BaseModel): + branchId: int + orderDate: str + name: str + description: str | None = None + status: str = "pending" + + +class UpdateOrderRequest(BaseModel): + branchId: int | None = None + orderDate: str | None = None + name: str | None = None + description: str | None = None + status: str | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail.py new file mode 100644 index 000000000..67db7c870 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class OrderDetail(BaseModel): + orderDetailId: int + orderId: int + productId: int + quantity: int + unitPrice: float + notes: str | None = None + + +class CreateOrderDetailRequest(BaseModel): + orderId: int + productId: int + quantity: int + unitPrice: float + notes: str | None = None + + +class UpdateOrderDetailRequest(BaseModel): + orderId: int | None = None + productId: int | None = None + quantity: int | None = None + unitPrice: float | None = None + notes: str | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail_delivery.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail_delivery.py new file mode 100644 index 000000000..b3d2c4d1e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/order_detail_delivery.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class OrderDetailDelivery(BaseModel): + orderDetailDeliveryId: int + orderDetailId: int + deliveryId: int + quantity: int + notes: str | None = None + + +class CreateOrderDetailDeliveryRequest(BaseModel): + orderDetailId: int + deliveryId: int + quantity: int + notes: str | None = None + + +class UpdateOrderDetailDeliveryRequest(BaseModel): + orderDetailId: int | None = None + deliveryId: int | None = None + quantity: int | None = None + notes: str | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/product.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/product.py new file mode 100644 index 000000000..c3c951223 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/product.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Product(BaseModel): + productId: int + supplierId: int + name: str + description: str | None = None + price: float + sku: str + unit: str + imgName: str | None = None + discount: float + + +class CreateProductRequest(BaseModel): + supplierId: int + name: str + description: str | None = None + price: float + sku: str + unit: str + imgName: str | None = None + discount: float = 0.0 + + +class UpdateProductRequest(BaseModel): + supplierId: int | None = None + name: str | None = None + description: str | None = None + price: float | None = None + sku: str | None = None + unit: str | None = None + imgName: str | None = None + discount: float | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/supplier.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/supplier.py new file mode 100644 index 000000000..51570db74 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/models/supplier.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Supplier(BaseModel): + supplierId: int + name: str + description: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + active: bool + verified: bool + + +class CreateSupplierRequest(BaseModel): + name: str + description: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + active: bool = True + verified: bool = False + + +class UpdateSupplierRequest(BaseModel): + name: str | None = None + description: str | None = None + contactPerson: str | None = None + email: str | None = None + phone: str | None = None + active: bool | None = None + verified: bool | None = None diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/__init__.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/__init__.py new file mode 100644 index 000000000..f7332970e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/__init__.py @@ -0,0 +1 @@ +"""Repository layer for SQLite persistence.""" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/_helpers.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/_helpers.py new file mode 100644 index 000000000..a20bd03ba --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/_helpers.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import Any + +import aiosqlite + +from utils.errors import ValidationException + + +def build_update_statement(field_to_column: dict[str, str], updates: dict[str, Any]) -> tuple[str, list[Any]]: + provided = {k: v for k, v in updates.items() if v is not None} + if not provided: + raise ValidationException("No fields provided for update") + + assignments: list[str] = [] + parameters: list[Any] = [] + for field_name, value in provided.items(): + assignments.append(f"{field_to_column[field_name]} = ?") + parameters.append(value) + + return ", ".join(assignments), parameters + + +async def execute_with_changes( + connection: aiosqlite.Connection, + sql: str, + params: tuple[Any, ...] | list[Any] = (), +) -> int: + cursor = await connection.execute(sql, params) + await connection.commit() + if cursor.rowcount is not None and cursor.rowcount >= 0: + return cursor.rowcount + + changes_cursor = await connection.execute("SELECT changes();") + row = await changes_cursor.fetchone() + return int(row[0]) if row else 0 diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/branches.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/branches.py new file mode 100644 index 000000000..09a9c6cf1 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/branches.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.branch import Branch, CreateBranchRequest, UpdateBranchRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class BranchesRepository: + async def find_all(self) -> list[Branch]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM branches ORDER BY branch_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, branch_id: int) -> Branch | None: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM branches WHERE branch_id = ?;", (branch_id,)) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateBranchRequest) -> Branch: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO branches (headquarters_id, name, description, address, contact_person, email, phone) + VALUES (?, ?, ?, ?, ?, ?, ?); + """, + ( + request.headquartersId, + request.name, + request.description, + request.address, + request.contactPerson, + request.email, + request.phone, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Branch") + return created + + async def update(self, branch_id: int, request: UpdateBranchRequest) -> Branch: + updates, parameters = build_update_statement( + { + "headquartersId": "headquarters_id", + "name": "name", + "description": "description", + "address": "address", + "contactPerson": "contact_person", + "email": "email", + "phone": "phone", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(branch_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE branches SET {updates} WHERE branch_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Branch", branch_id) + + updated = await self.find_by_id(branch_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Branch") + return updated + + async def delete(self, branch_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM branches WHERE branch_id = ?;", + (branch_id,), + ) + if changes == 0: + raise NotFoundException("Branch", branch_id) + + @staticmethod + def _map_row(row: object) -> Branch: + return Branch( + branchId=row["branch_id"], + headquartersId=row["headquarters_id"], + name=row["name"], + description=row["description"], + address=row["address"], + contactPerson=row["contact_person"], + email=row["email"], + phone=row["phone"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/deliveries.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/deliveries.py new file mode 100644 index 000000000..a21b8db57 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/deliveries.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.delivery import CreateDeliveryRequest, Delivery, UpdateDeliveryRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class DeliveriesRepository: + async def find_all(self) -> list[Delivery]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM deliveries ORDER BY delivery_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, delivery_id: int) -> Delivery | None: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM deliveries WHERE delivery_id = ?;", (delivery_id,)) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateDeliveryRequest) -> Delivery: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO deliveries (supplier_id, delivery_date, name, description, status) + VALUES (?, ?, ?, ?, ?); + """, + ( + request.supplierId, + request.deliveryDate, + request.name, + request.description, + request.status, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Delivery") + return created + + async def update(self, delivery_id: int, request: UpdateDeliveryRequest) -> Delivery: + updates, parameters = build_update_statement( + { + "supplierId": "supplier_id", + "deliveryDate": "delivery_date", + "name": "name", + "description": "description", + "status": "status", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(delivery_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE deliveries SET {updates} WHERE delivery_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Delivery", delivery_id) + + updated = await self.find_by_id(delivery_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Delivery") + return updated + + async def update_status(self, delivery_id: int, status: str) -> Delivery: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "UPDATE deliveries SET status = ? WHERE delivery_id = ?;", + (status, delivery_id), + ) + if changes == 0: + raise NotFoundException("Delivery", delivery_id) + + updated = await self.find_by_id(delivery_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Delivery") + return updated + + async def delete(self, delivery_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM deliveries WHERE delivery_id = ?;", + (delivery_id,), + ) + if changes == 0: + raise NotFoundException("Delivery", delivery_id) + + @staticmethod + def _map_row(row: object) -> Delivery: + return Delivery( + deliveryId=row["delivery_id"], + supplierId=row["supplier_id"], + deliveryDate=row["delivery_date"], + name=row["name"], + description=row["description"], + status=row["status"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/headquarters.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/headquarters.py new file mode 100644 index 000000000..76d990859 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/headquarters.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.headquarters import ( + CreateHeadquartersRequest, + Headquarters, + HeadquartersMetrics, + UpdateHeadquartersRequest, +) +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class HeadquartersRepository: + async def find_all(self) -> list[Headquarters]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM headquarters ORDER BY headquarters_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, headquarters_id: int) -> Headquarters | None: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM headquarters WHERE headquarters_id = ?;", + (headquarters_id,), + ) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def get_metrics(self, headquarters_id: int) -> HeadquartersMetrics: + headquarters = await self.find_by_id(headquarters_id) + if headquarters is None: + raise NotFoundException("Headquarters", headquarters_id) + + return HeadquartersMetrics( + score=headquarters.headquartersId, + average=headquarters.headquartersId / 2.0, + display=f"HQ-{headquarters.headquartersId}0", + ) + + async def create(self, request: CreateHeadquartersRequest) -> Headquarters: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO headquarters (name, description, address, contact_person, email, phone) + VALUES (?, ?, ?, ?, ?, ?); + """, + ( + request.name, + request.description, + request.address, + request.contactPerson, + request.email, + request.phone, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Headquarters") + return created + + async def update(self, headquarters_id: int, request: UpdateHeadquartersRequest) -> Headquarters: + updates, parameters = build_update_statement( + { + "name": "name", + "description": "description", + "address": "address", + "contactPerson": "contact_person", + "email": "email", + "phone": "phone", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(headquarters_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE headquarters SET {updates} WHERE headquarters_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Headquarters", headquarters_id) + + updated = await self.find_by_id(headquarters_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Headquarters") + return updated + + async def delete(self, headquarters_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM headquarters WHERE headquarters_id = ?;", + (headquarters_id,), + ) + if changes == 0: + raise NotFoundException("Headquarters", headquarters_id) + + @staticmethod + def _map_row(row: object) -> Headquarters: + return Headquarters( + headquartersId=row["headquarters_id"], + name=row["name"], + description=row["description"], + address=row["address"], + contactPerson=row["contact_person"], + email=row["email"], + phone=row["phone"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_detail_deliveries.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_detail_deliveries.py new file mode 100644 index 000000000..e55c71aad --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_detail_deliveries.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.order_detail_delivery import ( + CreateOrderDetailDeliveryRequest, + OrderDetailDelivery, + UpdateOrderDetailDeliveryRequest, +) +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class OrderDetailDeliveriesRepository: + async def find_all(self) -> list[OrderDetailDelivery]: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM order_detail_deliveries ORDER BY order_detail_delivery_id;" + ) + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, relation_id: int) -> OrderDetailDelivery | None: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM order_detail_deliveries WHERE order_detail_delivery_id = ?;", + (relation_id,), + ) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateOrderDetailDeliveryRequest) -> OrderDetailDelivery: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO order_detail_deliveries (order_detail_id, delivery_id, quantity, notes) + VALUES (?, ?, ?, ?); + """, + (request.orderDetailId, request.deliveryId, request.quantity, request.notes), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created OrderDetailDelivery") + return created + + async def update(self, relation_id: int, request: UpdateOrderDetailDeliveryRequest) -> OrderDetailDelivery: + updates, parameters = build_update_statement( + { + "orderDetailId": "order_detail_id", + "deliveryId": "delivery_id", + "quantity": "quantity", + "notes": "notes", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(relation_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE order_detail_deliveries SET {updates} WHERE order_detail_delivery_id = ?;", + parameters, + ) + if changes == 0: + raise NotFoundException("OrderDetailDelivery", relation_id) + + updated = await self.find_by_id(relation_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated OrderDetailDelivery") + return updated + + async def delete(self, relation_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM order_detail_deliveries WHERE order_detail_delivery_id = ?;", + (relation_id,), + ) + if changes == 0: + raise NotFoundException("OrderDetailDelivery", relation_id) + + @staticmethod + def _map_row(row: object) -> OrderDetailDelivery: + return OrderDetailDelivery( + orderDetailDeliveryId=row["order_detail_delivery_id"], + orderDetailId=row["order_detail_id"], + deliveryId=row["delivery_id"], + quantity=row["quantity"], + notes=row["notes"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_details.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_details.py new file mode 100644 index 000000000..c5c773a6e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/order_details.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.order_detail import CreateOrderDetailRequest, OrderDetail, UpdateOrderDetailRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class OrderDetailsRepository: + async def find_all(self) -> list[OrderDetail]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM order_details ORDER BY order_detail_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, order_detail_id: int) -> OrderDetail | None: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM order_details WHERE order_detail_id = ?;", + (order_detail_id,), + ) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateOrderDetailRequest) -> OrderDetail: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO order_details (order_id, product_id, quantity, unit_price, notes) + VALUES (?, ?, ?, ?, ?); + """, + ( + request.orderId, + request.productId, + request.quantity, + request.unitPrice, + request.notes, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created OrderDetail") + return created + + async def update(self, order_detail_id: int, request: UpdateOrderDetailRequest) -> OrderDetail: + updates, parameters = build_update_statement( + { + "orderId": "order_id", + "productId": "product_id", + "quantity": "quantity", + "unitPrice": "unit_price", + "notes": "notes", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(order_detail_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE order_details SET {updates} WHERE order_detail_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("OrderDetail", order_detail_id) + + updated = await self.find_by_id(order_detail_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated OrderDetail") + return updated + + async def delete(self, order_detail_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM order_details WHERE order_detail_id = ?;", + (order_detail_id,), + ) + if changes == 0: + raise NotFoundException("OrderDetail", order_detail_id) + + @staticmethod + def _map_row(row: object) -> OrderDetail: + return OrderDetail( + orderDetailId=row["order_detail_id"], + orderId=row["order_id"], + productId=row["product_id"], + quantity=row["quantity"], + unitPrice=row["unit_price"], + notes=row["notes"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/orders.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/orders.py new file mode 100644 index 000000000..6685e28f0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/orders.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.order import CreateOrderRequest, Order, UpdateOrderRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class OrdersRepository: + async def find_all(self) -> list[Order]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM orders ORDER BY order_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, order_id: int) -> Order | None: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM orders WHERE order_id = ?;", (order_id,)) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateOrderRequest) -> Order: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO orders (branch_id, order_date, name, description, status) + VALUES (?, ?, ?, ?, ?); + """, + ( + request.branchId, + request.orderDate, + request.name, + request.description, + request.status, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Order") + return created + + async def update(self, order_id: int, request: UpdateOrderRequest) -> Order: + updates, parameters = build_update_statement( + { + "branchId": "branch_id", + "orderDate": "order_date", + "name": "name", + "description": "description", + "status": "status", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(order_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE orders SET {updates} WHERE order_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Order", order_id) + + updated = await self.find_by_id(order_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Order") + return updated + + async def delete(self, order_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes(connection, "DELETE FROM orders WHERE order_id = ?;", (order_id,)) + if changes == 0: + raise NotFoundException("Order", order_id) + + @staticmethod + def _map_row(row: object) -> Order: + return Order( + orderId=row["order_id"], + branchId=row["branch_id"], + orderDate=row["order_date"], + name=row["name"], + description=row["description"], + status=row["status"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/products.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/products.py new file mode 100644 index 000000000..84f68b6a0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/products.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.product import CreateProductRequest, Product, UpdateProductRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class ProductsRepository: + async def find_all(self) -> list[Product]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM products ORDER BY product_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, product_id: int) -> Product | None: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM products WHERE product_id = ?;", + (product_id,), + ) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateProductRequest) -> Product: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO products (supplier_id, name, description, price, sku, unit, img_name, discount) + VALUES (?, ?, ?, ?, ?, ?, ?, ?); + """, + ( + request.supplierId, + request.name, + request.description, + request.price, + request.sku, + request.unit, + request.imgName, + request.discount, + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Product") + return created + + async def update(self, product_id: int, request: UpdateProductRequest) -> Product: + updates, parameters = build_update_statement( + { + "supplierId": "supplier_id", + "name": "name", + "description": "description", + "price": "price", + "sku": "sku", + "unit": "unit", + "imgName": "img_name", + "discount": "discount", + }, + request.model_dump(exclude_unset=True), + ) + parameters.append(product_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE products SET {updates} WHERE product_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Product", product_id) + + updated = await self.find_by_id(product_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Product") + return updated + + async def delete(self, product_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM products WHERE product_id = ?;", + (product_id,), + ) + if changes == 0: + raise NotFoundException("Product", product_id) + + @staticmethod + def _map_row(row: object) -> Product: + return Product( + productId=row["product_id"], + supplierId=row["supplier_id"], + name=row["name"], + description=row["description"], + price=row["price"], + sku=row["sku"], + unit=row["unit"], + imgName=row["img_name"], + discount=row["discount"], + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/suppliers.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/suppliers.py new file mode 100644 index 000000000..698e821d9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/repositories/suppliers.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from database.sqlite import get_connection +from models.supplier import CreateSupplierRequest, Supplier, UpdateSupplierRequest +from repositories._helpers import build_update_statement, execute_with_changes +from utils.errors import DatabaseException, NotFoundException + + +class SuppliersRepository: + async def find_all(self) -> list[Supplier]: + async with get_connection() as connection: + cursor = await connection.execute("SELECT * FROM suppliers ORDER BY supplier_id;") + rows = await cursor.fetchall() + return [self._map_row(row) for row in rows] + + async def find_by_id(self, supplier_id: int) -> Supplier | None: + async with get_connection() as connection: + cursor = await connection.execute( + "SELECT * FROM suppliers WHERE supplier_id = ?;", + (supplier_id,), + ) + row = await cursor.fetchone() + return self._map_row(row) if row else None + + async def create(self, request: CreateSupplierRequest) -> Supplier: + async with get_connection() as connection: + cursor = await connection.execute( + """ + INSERT INTO suppliers (name, description, contact_person, email, phone, active, verified) + VALUES (?, ?, ?, ?, ?, ?, ?); + """, + ( + request.name, + request.description, + request.contactPerson, + request.email, + request.phone, + int(request.active), + int(request.verified), + ), + ) + await connection.commit() + created_id = int(cursor.lastrowid) + + created = await self.find_by_id(created_id) + if created is None: + raise DatabaseException("Failed to retrieve created Supplier") + return created + + async def update(self, supplier_id: int, request: UpdateSupplierRequest) -> Supplier: + updates, parameters = build_update_statement( + { + "name": "name", + "description": "description", + "contactPerson": "contact_person", + "email": "email", + "phone": "phone", + "active": "active", + "verified": "verified", + }, + request.model_dump(exclude_unset=True), + ) + parameters = [int(p) if isinstance(p, bool) else p for p in parameters] + parameters.append(supplier_id) + + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + f"UPDATE suppliers SET {updates} WHERE supplier_id = ?;", + parameters, + ) + + if changes == 0: + raise NotFoundException("Supplier", supplier_id) + + updated = await self.find_by_id(supplier_id) + if updated is None: + raise DatabaseException("Failed to retrieve updated Supplier") + return updated + + async def delete(self, supplier_id: int) -> None: + async with get_connection() as connection: + changes = await execute_with_changes( + connection, + "DELETE FROM suppliers WHERE supplier_id = ?;", + (supplier_id,), + ) + if changes == 0: + raise NotFoundException("Supplier", supplier_id) + + @staticmethod + def _map_row(row: object) -> Supplier: + return Supplier( + supplierId=row["supplier_id"], + name=row["name"], + description=row["description"], + contactPerson=row["contact_person"], + email=row["email"], + phone=row["phone"], + active=bool(row["active"]), + verified=bool(row["verified"]), + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/requirements.txt b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/requirements.txt new file mode 100644 index 000000000..169c804a0 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.115.0,<1.0.0 +uvicorn>=0.30.0,<1.0.0 +aiosqlite>=0.20.0,<1.0.0 +pydantic>=2.7.0,<3.0.0 +pytest>=8.0.0,<9.0.0 +httpx>=0.27.0,<1.0.0 diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/__init__.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/__init__.py new file mode 100644 index 000000000..75959421a --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/__init__.py @@ -0,0 +1 @@ +"""FastAPI route modules.""" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/branches.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/branches.py new file mode 100644 index 000000000..8db8cc364 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/branches.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.branch import Branch, CreateBranchRequest, UpdateBranchRequest +from repositories.branches import BranchesRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/branches", tags=["Branches"]) +repository = BranchesRepository() + + +@router.get("", response_model=list[Branch]) +async def get_all() -> list[Branch]: + return await repository.find_all() + + +@router.get("/{branch_id}", response_model=Branch) +async def get_by_id(branch_id: int) -> Branch: + branch = await repository.find_by_id(branch_id) + if branch is None: + raise NotFoundException("Branch", branch_id) + return branch + + +@router.post("", response_model=Branch, status_code=201) +async def create(request: CreateBranchRequest) -> Branch: + if not request.name.strip(): + raise ValidationException("name is required") + if request.headquartersId <= 0: + raise ValidationException("headquartersId is required") + return await repository.create(request) + + +@router.put("/{branch_id}", response_model=Branch) +async def update(branch_id: int, request: UpdateBranchRequest) -> Branch: + return await repository.update(branch_id, request) + + +@router.delete("/{branch_id}", status_code=204) +async def delete(branch_id: int) -> None: + await repository.delete(branch_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/deliveries.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/deliveries.py new file mode 100644 index 000000000..b09327161 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/deliveries.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.delivery import ( + CreateDeliveryRequest, + Delivery, + UpdateDeliveryRequest, + UpdateDeliveryStatusRequest, +) +from repositories.deliveries import DeliveriesRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/deliveries", tags=["Deliveries"]) +repository = DeliveriesRepository() + + +@router.get("", response_model=list[Delivery]) +async def get_all() -> list[Delivery]: + return await repository.find_all() + + +@router.get("/{delivery_id}", response_model=Delivery) +async def get_by_id(delivery_id: int) -> Delivery: + delivery = await repository.find_by_id(delivery_id) + if delivery is None: + raise NotFoundException("Delivery", delivery_id) + return delivery + + +@router.post("", response_model=Delivery, status_code=201) +async def create(request: CreateDeliveryRequest) -> Delivery: + if not request.name.strip(): + raise ValidationException("name is required") + if not request.deliveryDate.strip(): + raise ValidationException("deliveryDate is required") + return await repository.create(request) + + +@router.put("/{delivery_id}", response_model=Delivery) +async def update(delivery_id: int, request: UpdateDeliveryRequest) -> Delivery: + return await repository.update(delivery_id, request) + + +@router.put("/{delivery_id}/status") +async def update_status( + delivery_id: int, + request: UpdateDeliveryStatusRequest, +) -> Delivery | dict[str, object]: + if not request.status.strip(): + raise ValidationException("status is required") + updated_delivery = await repository.update_status(delivery_id, request.status) + if request.deliveryPartner and request.deliveryPartner.strip(): + return {"delivery": updated_delivery, "commandOutput": ""} + return updated_delivery + + +@router.delete("/{delivery_id}", status_code=204) +async def delete(delivery_id: int) -> None: + await repository.delete(delivery_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/headquarters.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/headquarters.py new file mode 100644 index 000000000..3a266d935 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/headquarters.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.headquarters import ( + CreateHeadquartersRequest, + Headquarters, + HeadquartersMetrics, + UpdateHeadquartersRequest, +) +from repositories.headquarters import HeadquartersRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/headquarters", tags=["Headquarters"]) +repository = HeadquartersRepository() + + +@router.get("", response_model=list[Headquarters]) +async def get_all() -> list[Headquarters]: + return await repository.find_all() + + +@router.get("/{headquarters_id}", response_model=Headquarters) +async def get_by_id(headquarters_id: int) -> Headquarters: + headquarters = await repository.find_by_id(headquarters_id) + if headquarters is None: + raise NotFoundException("Headquarters", headquarters_id) + return headquarters + + +@router.get("/{headquarters_id}/metrics", response_model=HeadquartersMetrics) +async def get_metrics(headquarters_id: int) -> HeadquartersMetrics: + return await repository.get_metrics(headquarters_id) + + +@router.get("/{headquarters_id}/label") +async def get_label(headquarters_id: int) -> dict[str, str]: + headquarters = await repository.find_by_id(headquarters_id) + if headquarters is None: + raise NotFoundException("Headquarters", headquarters_id) + return {"label": f"Location:{headquarters.name}City:Country:"} + + +@router.post("", response_model=Headquarters, status_code=201) +async def create(request: CreateHeadquartersRequest) -> Headquarters: + if not request.name.strip(): + raise ValidationException("name is required") + return await repository.create(request) + + +@router.put("/{headquarters_id}", response_model=Headquarters) +async def update(headquarters_id: int, request: UpdateHeadquartersRequest) -> Headquarters: + return await repository.update(headquarters_id, request) + + +@router.delete("/{headquarters_id}", status_code=204) +async def delete(headquarters_id: int) -> None: + await repository.delete(headquarters_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_detail_deliveries.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_detail_deliveries.py new file mode 100644 index 000000000..6f88848f3 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_detail_deliveries.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.order_detail_delivery import ( + CreateOrderDetailDeliveryRequest, + OrderDetailDelivery, + UpdateOrderDetailDeliveryRequest, +) +from repositories.order_detail_deliveries import OrderDetailDeliveriesRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/order-detail-deliveries", tags=["Order Detail Deliveries"]) +repository = OrderDetailDeliveriesRepository() + + +@router.get("", response_model=list[OrderDetailDelivery]) +async def get_all() -> list[OrderDetailDelivery]: + return await repository.find_all() + + +@router.get("/{relation_id}", response_model=OrderDetailDelivery) +async def get_by_id(relation_id: int) -> OrderDetailDelivery: + relation = await repository.find_by_id(relation_id) + if relation is None: + raise NotFoundException("OrderDetailDelivery", relation_id) + return relation + + +@router.post("", response_model=OrderDetailDelivery, status_code=201) +async def create(request: CreateOrderDetailDeliveryRequest) -> OrderDetailDelivery: + if request.orderDetailId <= 0 or request.deliveryId <= 0: + raise ValidationException("orderDetailId and deliveryId are required") + return await repository.create(request) + + +@router.put("/{relation_id}", response_model=OrderDetailDelivery) +async def update(relation_id: int, request: UpdateOrderDetailDeliveryRequest) -> OrderDetailDelivery: + return await repository.update(relation_id, request) + + +@router.delete("/{relation_id}", status_code=204) +async def delete(relation_id: int) -> None: + await repository.delete(relation_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_details.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_details.py new file mode 100644 index 000000000..9d8bb570b --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/order_details.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.order_detail import CreateOrderDetailRequest, OrderDetail, UpdateOrderDetailRequest +from repositories.order_details import OrderDetailsRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/order-details", tags=["Order Details"]) +repository = OrderDetailsRepository() + + +@router.get("", response_model=list[OrderDetail]) +async def get_all() -> list[OrderDetail]: + return await repository.find_all() + + +@router.get("/{order_detail_id}", response_model=OrderDetail) +async def get_by_id(order_detail_id: int) -> OrderDetail: + order_detail = await repository.find_by_id(order_detail_id) + if order_detail is None: + raise NotFoundException("OrderDetail", order_detail_id) + return order_detail + + +@router.post("", response_model=OrderDetail, status_code=201) +async def create(request: CreateOrderDetailRequest) -> OrderDetail: + if request.orderId <= 0 or request.productId <= 0: + raise ValidationException("orderId and productId are required") + return await repository.create(request) + + +@router.put("/{order_detail_id}", response_model=OrderDetail) +async def update(order_detail_id: int, request: UpdateOrderDetailRequest) -> OrderDetail: + return await repository.update(order_detail_id, request) + + +@router.delete("/{order_detail_id}", status_code=204) +async def delete(order_detail_id: int) -> None: + await repository.delete(order_detail_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/orders.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/orders.py new file mode 100644 index 000000000..0e67d1238 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/orders.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.order import CreateOrderRequest, Order, UpdateOrderRequest +from repositories.orders import OrdersRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/orders", tags=["Orders"]) +repository = OrdersRepository() + + +@router.get("", response_model=list[Order]) +async def get_all() -> list[Order]: + return await repository.find_all() + + +@router.get("/{order_id}", response_model=Order) +async def get_by_id(order_id: int) -> Order: + order = await repository.find_by_id(order_id) + if order is None: + raise NotFoundException("Order", order_id) + return order + + +@router.post("", response_model=Order, status_code=201) +async def create(request: CreateOrderRequest) -> Order: + if not request.name.strip(): + raise ValidationException("name is required") + if request.branchId <= 0: + raise ValidationException("branchId is required") + if not request.orderDate.strip(): + raise ValidationException("orderDate is required") + return await repository.create(request) + + +@router.put("/{order_id}", response_model=Order) +async def update(order_id: int, request: UpdateOrderRequest) -> Order: + return await repository.update(order_id, request) + + +@router.delete("/{order_id}", status_code=204) +async def delete(order_id: int) -> None: + await repository.delete(order_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/products.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/products.py new file mode 100644 index 000000000..3180d4988 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/products.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.product import CreateProductRequest, Product, UpdateProductRequest +from repositories.products import ProductsRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/products", tags=["Products"]) +repository = ProductsRepository() + + +@router.get("", response_model=list[Product]) +async def get_all() -> list[Product]: + return await repository.find_all() + + +@router.get("/{product_id}", response_model=Product) +async def get_by_id(product_id: int) -> Product: + product = await repository.find_by_id(product_id) + if product is None: + raise NotFoundException("Product", product_id) + return product + + +@router.post("", response_model=Product, status_code=201) +async def create(request: CreateProductRequest) -> Product: + if not request.name.strip(): + raise ValidationException("name is required") + return await repository.create(request) + + +@router.put("/{product_id}", response_model=Product) +async def update(product_id: int, request: UpdateProductRequest) -> Product: + return await repository.update(product_id, request) + + +@router.delete("/{product_id}", status_code=204) +async def delete(product_id: int) -> None: + await repository.delete(product_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/suppliers.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/suppliers.py new file mode 100644 index 000000000..787353340 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/routes/suppliers.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from models.supplier import CreateSupplierRequest, Supplier, UpdateSupplierRequest +from repositories.suppliers import SuppliersRepository +from utils.errors import NotFoundException, ValidationException + +router = APIRouter(prefix="/api/suppliers", tags=["Suppliers"]) +repository = SuppliersRepository() + + +@router.get("", response_model=list[Supplier]) +async def get_all() -> list[Supplier]: + return await repository.find_all() + + +@router.get("/{supplier_id}", response_model=Supplier) +async def get_by_id(supplier_id: int) -> Supplier: + supplier = await repository.find_by_id(supplier_id) + if supplier is None: + raise NotFoundException("Supplier", supplier_id) + return supplier + + +@router.get("/{supplier_id}/status") +async def get_status(supplier_id: int) -> dict[str, str]: + supplier = await repository.find_by_id(supplier_id) + if supplier is None: + raise NotFoundException("Supplier", supplier_id) + return {"status": "APPROVED"} + + +@router.post("", response_model=Supplier, status_code=201) +async def create(request: CreateSupplierRequest) -> Supplier: + if not request.name.strip(): + raise ValidationException("name is required") + return await repository.create(request) + + +@router.put("/{supplier_id}", response_model=Supplier) +async def update(supplier_id: int, request: UpdateSupplierRequest) -> Supplier: + return await repository.update(supplier_id, request) + + +@router.delete("/{supplier_id}", status_code=204) +async def delete(supplier_id: int) -> None: + await repository.delete(supplier_id) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/tests/test_main.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/tests/test_main.py new file mode 100644 index 000000000..167fc5e5e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/tests/test_main.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + +from main import app + + +def test_root() -> None: + with TestClient(app) as client: + response = client.get("/") + assert response.status_code == 200 + assert response.json() == "Hello, world!" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/__init__.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/__init__.py new file mode 100644 index 000000000..f419f5515 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/__init__.py @@ -0,0 +1 @@ +"""Shared utility modules.""" diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/errors.py b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/errors.py new file mode 100644 index 000000000..ce9edc04f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-py/utils/errors.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + + +class ApiException(Exception): + def __init__(self, message: str, code: str, status_code: int) -> None: + super().__init__(message) + self.code = code + self.status_code = status_code + + +class NotFoundException(ApiException): + def __init__(self, entity: str, entity_id: int) -> None: + super().__init__(f"{entity} with ID {entity_id} not found", "NOT_FOUND", 404) + + +class ValidationException(ApiException): + def __init__(self, message: str) -> None: + super().__init__(f"Validation error: {message}", "VALIDATION_ERROR", 400) + + +class ConflictException(ApiException): + def __init__(self, message: str) -> None: + super().__init__(f"Conflict: {message}", "CONFLICT", 409) + + +class DatabaseException(ApiException): + def __init__(self, message: str) -> None: + super().__init__(message, "DATABASE_ERROR", 500) + + +def register_exception_handlers(app: FastAPI) -> None: + @app.exception_handler(ApiException) + async def handle_api_exception(_: Request, exc: ApiException) -> JSONResponse: + return JSONResponse( + status_code=exc.status_code, + content={"error": {"code": exc.code, "message": str(exc)}}, + ) + + @app.exception_handler(Exception) + async def handle_unexpected_exception(_: Request, __: Exception) -> JSONResponse: + return JSONResponse( + status_code=500, + content={ + "error": { + "code": "INTERNAL_ERROR", + "message": "An unexpected error occurred", + } + }, + ) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.dockerignore b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.dockerignore new file mode 100644 index 000000000..fa9992069 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.dockerignore @@ -0,0 +1,61 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Development files +.env* +!.env.example + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Testing +coverage/ +test-results/ +*.tgz + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Documentation +README.md +docs/ +*.md + +# Database files (will be created in container) +data/ +*.db +*.sqlite +*.sqlite3 + +# Temporary files +tmp/ +temp/ + +# Build cache +.cache/ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.gitignore b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.gitignore new file mode 100644 index 000000000..58851fcf2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +*.db +*.db-shm +*.db-wal +coverage/ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/Dockerfile b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/Dockerfile new file mode 100644 index 000000000..485db78c4 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/Dockerfile @@ -0,0 +1,17 @@ +FROM node:24-alpine AS builder + +WORKDIR /app +COPY ./package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:24-alpine +WORKDIR /app +COPY ./package*.json ./ +RUN npm install --omit=dev +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/database ./database + +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ERD.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ERD.png new file mode 100644 index 000000000..ba8042051 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ERD.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/README.md new file mode 100644 index 000000000..916ebb10e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/README.md @@ -0,0 +1,44 @@ +# OctoSupply API (TypeScript) + +Express + TypeScript reference implementation of the OctoSupply API. The C#, Python, and Java variants are ports of this service. + +## Run + +```bash +npm install +npm run dev +``` + +Default API URL: `http://localhost:3000` + +## Environment + +- `PORT` (default: `3000`) +- `DB_FILE` (default: `./data/app.db`) +- `API_CORS_ORIGINS` (comma-separated override; optional) +- `DB_ENABLE_WAL` (enable WAL mode; default: `true`) +- `DB_FOREIGN_KEYS` (enforce foreign keys; default: `true`) +- `DB_TIMEOUT` (busy timeout in ms; default: `30000`) + +## Database + +On startup, the API applies SQL migrations from: + +- `../database/migrations/*.sql` + +and seeds from: + +- `../database/seed/*.sql` + +## OpenAPI + +- OpenAPI JSON: `http://localhost:3000/api-docs.json` +- Swagger UI: `http://localhost:3000/api-docs` + +## Tests + +Run TypeScript tests from the `src/` directory: + +```bash +make test-ts +``` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/api-swagger.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/api-swagger.json new file mode 100644 index 000000000..dc5894e1f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/api-swagger.json @@ -0,0 +1,1811 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Express API with Swagger", + "version": "1.0.0", + "description": "REST API documentation using Swagger/OpenAPI" + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "Development server" + } + ], + "paths": { + "/api/branches": { + "get": { + "summary": "Returns all branches", + "tags": [ + "Branches" + ], + "responses": { + "200": { + "description": "List of all branches", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Branch" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new branch", + "tags": [ + "Branches" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Branch" + } + } + } + }, + "responses": { + "201": { + "description": "Branch created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Branch" + } + } + } + } + } + } + }, + "/api/branches/{id}": { + "get": { + "summary": "Get a branch by ID", + "tags": [ + "Branches" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Branch ID" + } + ], + "responses": { + "200": { + "description": "Branch found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Branch" + } + } + } + }, + "404": { + "description": "Branch not found" + } + } + }, + "put": { + "summary": "Update a branch", + "tags": [ + "Branches" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Branch ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Branch" + } + } + } + }, + "responses": { + "200": { + "description": "Branch updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Branch" + } + } + } + }, + "404": { + "description": "Branch not found" + } + } + }, + "delete": { + "summary": "Delete a branch", + "tags": [ + "Branches" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Branch ID" + } + ], + "responses": { + "204": { + "description": "Branch deleted successfully" + }, + "404": { + "description": "Branch not found" + } + } + } + }, + "/api/deliveries": { + "get": { + "summary": "Returns all deliveries", + "tags": [ + "Deliveries" + ], + "responses": { + "200": { + "description": "List of all deliveries", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Delivery" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new delivery", + "tags": [ + "Deliveries" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Delivery" + } + } + } + }, + "responses": { + "201": { + "description": "Delivery created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Delivery" + } + } + } + } + } + } + }, + "/api/deliveries/{id}": { + "get": { + "summary": "Get a delivery by ID", + "tags": [ + "Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Delivery ID" + } + ], + "responses": { + "200": { + "description": "Delivery found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Delivery" + } + } + } + }, + "404": { + "description": "Delivery not found" + } + } + }, + "put": { + "summary": "Update a delivery", + "tags": [ + "Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Delivery ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Delivery" + } + } + } + }, + "responses": { + "200": { + "description": "Delivery updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Delivery" + } + } + } + }, + "404": { + "description": "Delivery not found" + } + } + }, + "delete": { + "summary": "Delete a delivery", + "tags": [ + "Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Delivery ID" + } + ], + "responses": { + "204": { + "description": "Delivery deleted successfully" + }, + "404": { + "description": "Delivery not found" + } + } + } + }, + "/api/deliveries/{id}/status": { + "put": { + "summary": "Update the status of a delivery", + "tags": [ + "Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Delivery ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "The new status of the delivery" + }, + "deliveryPartner": { + "type": "string", + "description": "Optional delivery partner to notify via the notify-service about the status update" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Delivery status updated successfully", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Delivery" + }, + { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Delivery" + }, + "commandOutput": { + "type": "string", + "description": "Output from the notify-service command" + } + } + } + ] + } + } + } + }, + "404": { + "description": "Delivery not found" + }, + "500": { + "description": "Error executing notify command" + } + } + } + }, + "/api/headquarters": { + "get": { + "summary": "Returns all headquarters", + "tags": [ + "Headquarters" + ], + "responses": { + "200": { + "description": "List of all headquarters", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new headquarters", + "tags": [ + "Headquarters" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + }, + "responses": { + "201": { + "description": "Headquarters created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + } + } + } + }, + "/api/headquarters/{id}": { + "get": { + "summary": "Get a headquarters by ID", + "tags": [ + "Headquarters" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Headquarters ID" + } + ], + "responses": { + "200": { + "description": "Headquarters found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + }, + "404": { + "description": "Headquarters not found" + } + } + }, + "put": { + "summary": "Update a headquarters", + "tags": [ + "Headquarters" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Headquarters ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + }, + "responses": { + "200": { + "description": "Headquarters updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Headquarters" + } + } + } + }, + "404": { + "description": "Headquarters not found" + } + } + }, + "delete": { + "summary": "Delete a headquarters", + "tags": [ + "Headquarters" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Headquarters ID" + } + ], + "responses": { + "204": { + "description": "Headquarters deleted successfully" + }, + "404": { + "description": "Headquarters not found" + } + } + } + }, + "/api/headquarters/{id}/metrics": { + "get": { + "summary": "Get headquarters metrics by ID", + "tags": [ + "Headquarters" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Headquarters ID" + } + ], + "responses": { + "200": { + "description": "Headquarters metrics calculated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "Total score calculated from ID, floor count, and capacity" + }, + "average": { + "type": "number", + "description": "Average value" + }, + "display": { + "type": "string", + "description": "Display text" + } + } + } + } + } + }, + "404": { + "description": "Headquarters not found" + } + } + } + }, + "/api/headquarters/{id}/label": { + "get": { + "summary": "Get headquarters label by ID", + "tags": [ + "Headquarters" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Headquarters ID" + } + ], + "responses": { + "200": { + "description": "Headquarters label created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Formatted location label" + } + } + } + } + } + }, + "404": { + "description": "Headquarters not found" + } + } + } + }, + "/api/orders": { + "get": { + "summary": "Returns all orders", + "tags": [ + "Orders" + ], + "responses": { + "200": { + "description": "List of all orders", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new order", + "tags": [ + "Orders" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "201": { + "description": "Order created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + } + } + } + }, + "/api/orders/{id}": { + "get": { + "summary": "Get an order by ID", + "tags": [ + "Orders" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order ID" + } + ], + "responses": { + "200": { + "description": "Order found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "404": { + "description": "Order not found" + } + } + }, + "put": { + "summary": "Update an order", + "tags": [ + "Orders" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "200": { + "description": "Order updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "summary": "Delete an order", + "tags": [ + "Orders" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order ID" + } + ], + "responses": { + "204": { + "description": "Order deleted successfully" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/api/order-details": { + "get": { + "summary": "Returns all order details", + "tags": [ + "Order Details" + ], + "responses": { + "200": { + "description": "List of all order details", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new order detail", + "tags": [ + "Order Details" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + }, + "responses": { + "201": { + "description": "Order detail created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + } + } + } + }, + "/api/order-details/{id}": { + "get": { + "summary": "Get an order detail by ID", + "tags": [ + "Order Details" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail ID" + } + ], + "responses": { + "200": { + "description": "Order detail found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + }, + "404": { + "description": "Order detail not found" + } + } + }, + "put": { + "summary": "Update an order detail", + "tags": [ + "Order Details" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + }, + "responses": { + "200": { + "description": "Order detail updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetail" + } + } + } + }, + "404": { + "description": "Order detail not found" + } + } + }, + "delete": { + "summary": "Delete an order detail", + "tags": [ + "Order Details" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail ID" + } + ], + "responses": { + "204": { + "description": "Order detail deleted successfully" + }, + "404": { + "description": "Order detail not found" + } + } + } + }, + "/api/order-detail-deliveries": { + "get": { + "summary": "Returns all order detail deliveries", + "tags": [ + "Order Detail Deliveries" + ], + "responses": { + "200": { + "description": "List of all order detail deliveries", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new order detail delivery", + "tags": [ + "Order Detail Deliveries" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + }, + "responses": { + "201": { + "description": "Order detail delivery created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + } + } + } + }, + "/api/order-detail-deliveries/{id}": { + "get": { + "summary": "Get an order detail delivery by ID", + "tags": [ + "Order Detail Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail delivery ID" + } + ], + "responses": { + "200": { + "description": "Order detail delivery found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + }, + "404": { + "description": "Order detail delivery not found" + } + } + }, + "put": { + "summary": "Update an order detail delivery", + "tags": [ + "Order Detail Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail delivery ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + }, + "responses": { + "200": { + "description": "Order detail delivery updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetailDelivery" + } + } + } + }, + "404": { + "description": "Order detail delivery not found" + } + } + }, + "delete": { + "summary": "Delete an order detail delivery", + "tags": [ + "Order Detail Deliveries" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Order detail delivery ID" + } + ], + "responses": { + "204": { + "description": "Order detail delivery deleted successfully" + }, + "404": { + "description": "Order detail delivery not found" + } + } + } + }, + "/api/products": { + "get": { + "summary": "Returns all products", + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "List of all products", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new product", + "tags": [ + "Products" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "responses": { + "201": { + "description": "Product created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "/api/products/{id}": { + "get": { + "summary": "Get a product by ID", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Product ID" + } + ], + "responses": { + "200": { + "description": "Product found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "404": { + "description": "Product not found" + } + } + }, + "put": { + "summary": "Update a product", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Product ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "responses": { + "200": { + "description": "Product updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "404": { + "description": "Product not found" + } + } + }, + "delete": { + "summary": "Delete a product", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Product ID" + } + ], + "responses": { + "204": { + "description": "Product deleted successfully" + }, + "404": { + "description": "Product not found" + } + } + } + }, + "/api/suppliers": { + "get": { + "summary": "Returns all suppliers", + "tags": [ + "Suppliers" + ], + "responses": { + "200": { + "description": "List of all suppliers", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Supplier" + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new supplier", + "tags": [ + "Suppliers" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Supplier" + } + } + } + }, + "responses": { + "201": { + "description": "Supplier created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Supplier" + } + } + } + } + } + } + }, + "/api/suppliers/{id}": { + "get": { + "summary": "Get a supplier by ID", + "tags": [ + "Suppliers" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Supplier ID" + } + ], + "responses": { + "200": { + "description": "Supplier found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Supplier" + } + } + } + }, + "404": { + "description": "Supplier not found" + } + } + }, + "put": { + "summary": "Update a supplier", + "tags": [ + "Suppliers" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Supplier ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Supplier" + } + } + } + }, + "responses": { + "200": { + "description": "Supplier updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Supplier" + } + } + } + }, + "404": { + "description": "Supplier not found" + } + } + }, + "delete": { + "summary": "Delete a supplier", + "tags": [ + "Suppliers" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Supplier ID" + } + ], + "responses": { + "204": { + "description": "Supplier deleted successfully" + }, + "404": { + "description": "Supplier not found" + } + } + } + }, + "/api/suppliers/{id}/status": { + "get": { + "summary": "Get the status of a supplier", + "tags": [ + "Suppliers" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Supplier ID" + } + ], + "responses": { + "200": { + "description": "Supplier status", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "APPROVED", + "PENDING" + ] + } + } + } + } + } + }, + "404": { + "description": "Supplier not found" + } + } + } + } + }, + "components": { + "schemas": { + "Branch": { + "type": "object", + "required": [ + "branchId", + "name", + "headquartersId" + ], + "properties": { + "branchId": { + "type": "integer", + "description": "The unique identifier for the branch" + }, + "headquartersId": { + "type": "integer", + "description": "The ID of the headquarters this branch belongs to" + }, + "name": { + "type": "string", + "description": "The name of the branch" + }, + "address": { + "type": "string", + "description": "Physical address of the branch" + }, + "phone": { + "type": "string", + "description": "Contact phone number for the branch" + }, + "description": { + "type": "string", + "description": "Additional details about the branch" + }, + "contactPerson": { + "type": "string", + "description": "Name of the primary contact person" + }, + "email": { + "type": "string", + "format": "email", + "description": "Contact email for the branch" + } + } + }, + "Delivery": { + "type": "object", + "required": [ + "deliveryId", + "supplierId", + "status" + ], + "properties": { + "deliveryId": { + "type": "integer", + "description": "The unique identifier for the delivery" + }, + "supplierId": { + "type": "integer", + "description": "The ID of the supplier for this delivery" + }, + "deliveryDate": { + "type": "string", + "description": "The date of the delivery" + }, + "name": { + "type": "string", + "description": "The name of the delivery" + }, + "description": { + "type": "string", + "description": "Additional details about the delivery" + }, + "status": { + "type": "string", + "description": "Current status of the delivery" + } + } + }, + "Headquarters": { + "type": "object", + "required": [ + "headquartersId", + "name" + ], + "properties": { + "headquartersId": { + "type": "integer", + "description": "The unique identifier for the headquarters" + }, + "name": { + "type": "string", + "description": "The name of the headquarters" + }, + "address": { + "type": "string", + "description": "Main office address of the headquarters" + }, + "phone": { + "type": "string", + "description": "Contact phone number for the headquarters" + }, + "description": { + "type": "string", + "description": "Additional details about the headquarters" + }, + "contactPerson": { + "type": "string", + "description": "Name of the primary contact person" + }, + "email": { + "type": "string", + "format": "email", + "description": "Contact email for the headquarters" + }, + "city": { + "type": "string", + "description": "City where the headquarters is located" + }, + "country": { + "type": "string", + "description": "Country where the headquarters is located" + }, + "floorCount": { + "type": "integer", + "description": "Number of floors in the headquarters building" + }, + "capacity": { + "type": "integer", + "description": "Total capacity of the headquarters" + } + } + }, + "Order": { + "type": "object", + "required": [ + "orderId", + "branchId", + "orderDate" + ], + "properties": { + "orderId": { + "type": "integer", + "description": "The unique identifier for the order" + }, + "branchId": { + "type": "integer", + "description": "The ID of the branch that placed the order" + }, + "orderDate": { + "type": "string", + "format": "date-time", + "description": "The date and time when the order was placed" + }, + "name": { + "type": "string", + "description": "The name of the order" + }, + "description": { + "type": "string", + "description": "Additional details about the order" + }, + "status": { + "type": "string", + "description": "The current status of the order", + "enum": [ + "pending", + "processing", + "shipped", + "delivered", + "cancelled" + ] + } + } + }, + "OrderDetail": { + "type": "object", + "required": [ + "orderDetailId", + "orderId", + "productId", + "quantity", + "unitPrice" + ], + "properties": { + "orderDetailId": { + "type": "integer", + "description": "The unique identifier for the order detail" + }, + "orderId": { + "type": "integer", + "description": "The ID of the parent order" + }, + "productId": { + "type": "integer", + "description": "The ID of the product ordered" + }, + "quantity": { + "type": "integer", + "description": "The quantity of products ordered" + }, + "unitPrice": { + "type": "number", + "format": "float", + "description": "The price per unit" + }, + "notes": { + "type": "string", + "description": "Additional notes for the order detail" + } + } + }, + "OrderDetailDelivery": { + "type": "object", + "required": [ + "orderDetailDeliveryId", + "orderDetailId", + "deliveryId" + ], + "properties": { + "orderDetailDeliveryId": { + "type": "integer", + "description": "The unique identifier for the order detail delivery" + }, + "orderDetailId": { + "type": "integer", + "description": "The ID of the related order detail" + }, + "deliveryId": { + "type": "integer", + "description": "The ID of the related delivery" + }, + "quantity": { + "type": "integer", + "description": "The quantity of items in this delivery" + }, + "notes": { + "type": "string", + "description": "Additional notes about this delivery" + } + } + }, + "Product": { + "type": "object", + "required": [ + "productId", + "supplierId", + "name", + "price", + "sku", + "unit" + ], + "properties": { + "productId": { + "type": "integer", + "description": "The unique identifier for the product" + }, + "supplierId": { + "type": "integer", + "description": "The ID of the supplier providing this product" + }, + "name": { + "type": "string", + "description": "The name of the product" + }, + "description": { + "type": "string", + "description": "Detailed description of the product" + }, + "price": { + "type": "number", + "format": "float", + "description": "The current price of the product" + }, + "sku": { + "type": "string", + "description": "Stock keeping unit code for the product" + }, + "unit": { + "type": "string", + "description": "Unit of measure for the product (e.g., \"piece\")" + }, + "imgName": { + "type": "string", + "description": "Filename of the product image" + }, + "discount": { + "type": "number", + "format": "float", + "description": "Discount percentage (if applicable) expressed as a decimal (e.g., 0.25 for 25%)" + } + } + }, + "Supplier": { + "type": "object", + "required": [ + "supplierId", + "name" + ], + "properties": { + "supplierId": { + "type": "integer", + "description": "The unique identifier for the supplier" + }, + "name": { + "type": "string", + "description": "The name of the supplier" + }, + "contactPerson": { + "type": "string", + "description": "Name of the primary contact person" + }, + "email": { + "type": "string", + "format": "email", + "description": "Contact email for the supplier" + }, + "phone": { + "type": "string", + "description": "Contact phone number for the supplier" + }, + "description": { + "type": "string", + "description": "Additional details about the supplier" + }, + "active": { + "type": "boolean", + "description": "Whether the supplier is active" + }, + "verified": { + "type": "boolean", + "description": "Whether the supplier is verified" + } + } + } + } + }, + "tags": [ + { + "name": "Branches", + "description": "API endpoints for managing branches" + }, + { + "name": "Deliveries", + "description": "API endpoints for managing deliveries" + }, + { + "name": "Headquarters", + "description": "API endpoints for managing headquarters locations" + }, + { + "name": "Orders", + "description": "API endpoints for managing orders" + }, + { + "name": "Order Details", + "description": "API endpoints for managing order details" + }, + { + "name": "Order Detail Deliveries", + "description": "API endpoints for managing order detail deliveries" + }, + { + "name": "Products", + "description": "API endpoints for managing products" + }, + { + "name": "Suppliers", + "description": "API endpoints for managing suppliers" + } + ] +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ca.key b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ca.key new file mode 100644 index 000000000..0d74115a6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0ME8Adm8EG05n/1oEdWYEUDdBLfnB6adPZXAUTotJTDflOPf +9WP/U1ACoCoo5RMITTNADIME4ZYBrUxp2FgLYW/G39VSg4lZzN4y47eS25rJuTfH +Mklw5Kg+Ns8FR9C5FDsacEmyPIcqA9ciw/rCyFCixhmcc8lxlIXX7o31oYmmxX+6 +R0sPygkWrem/sP0pj+BsAV5WTBbB3H12q19PRJMZc45mcsDPxmZYfZYi35+3v70Q +i/WkhQzoo6oA2aHHHKK2+Rrtd039kSfBX/4iz3L/o3Q0ZoL0FZagI0CC+5cSnRH7 +mq820KQvyiQtoJzRr8Hzjd7eTt5a4jwRLcOGDQIDAQABAoIBACZm5bsRat86uJcN +7s8ZE9hYrk/n5MArjlF98tr+cL+etgKVyOVDd/zDgzgjiVJapfRNsUKb95HoHnba +z73UtINAJL2YaI15/uMJHSN26bUsTF+eOy6tA++MY6WBf98uLl3iYYK2i+tGkhwS +v3p97scazlbS70z9ib9gv9BKnR0R+DTDBACwNtfurOGQh9PDU/e3orsrVBR/kj7o +nfjlXZzsuuVdGRHmO2yGoALCx1N0dMpO/ALWDi+phP5Jz6SBo0AGAKfC2tXZJrVz +qwHfCPnklIphHHmFkArmrAYZDOHBNtLRFQL8SmfcZOz3HO9er87ct/zAHN5im91s +vVZnYQECgYEA90u/q4Ux9A2iaJ4qfDqjM8i+DLezj0ogLLe9OZuBgKfy41bt7ilX +4iQ6cmmzq/9x0dM2ydRXTVJ0Ek/0EDgfVxxcWTRSHHrwy+WOnaB0hWkMzwgDak58 +fhRi5RAhXhCJlUyHsU7FBTYcwQE6I/C971X0AuCH06eEeaHG8HOERzkCgYEA2Bo2 +cYvRmfL7uh4STWTJkq08ppNDgo1aJ8brc+YtK3v11OZEbnSSmq0rdnDqkvFBmSrJ +wHprhs+RGBKBJzI3BMZ9uxOzhufk/xPZrpZie77JFvpzMSbC0WMTpb06QsGk1bu7 +jqtbNx8OYYF7PVxiuUsZY3sOIZ5b3t9yWeiFwXUCgYB8tcyRGPiaFQ4kKC9Qutl2 +0fNVwoZg6obTRk288XkbgpbwovQWOO9C8fYvoLKlOIsTv6pPmi/0pHI4ke2JCGR1 +r626prIJ/s3UZY3IXBSm+tUkyuu9/pq1kl5VGg9ZuolHq3J6rjiZajKR+qZxXYTL +X9NQaB7XVBFwrW7/76FzsQKBgCOOVIzkI22AFDjwP7SqM5xFkqgZrM7rMP1AdncQ +VThFYhJQfMvrtD9s5KzNMVtSBKgN6ToZKl35AveB++wWEAViH0fLmwtEVmI9wuA9 +8CBKKM32EUPyC7Xl5lKrys03DUbggV5Z4e23AA6xOP4KO3UqI2yNJAwrAeOBbGq9Cak +4nUNAoGBAKBhG767ePf/5fMrKJnsdxK2+fpP1UglHHTKOxwtLU9xtkUw96g7yzxj +5ma66yv1QuGIvAmbLVLCI6MmFvXYQmomoRt2KnYEgxjg8jjXXDdoOsNL87t7l0HA +CviL/UR7ZZV28rp5TmexRworu7hC4qer9NZxqV0a7bOLq0+uVda9 +-----END RSA PRIVATE KEY----- diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/eslint.config.mjs b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/eslint.config.mjs new file mode 100644 index 000000000..ff9a37866 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/eslint.config.mjs @@ -0,0 +1,58 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import { defineConfig } from "eslint/config"; + + +export default defineConfig( + // Global ignores + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**'] + }, + // Base config for all TypeScript files + { + extends: [ + js.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.strict + ], + files: ['**/*.{ts,js}'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.node, + ...globals.es2022 + } + }, + rules: { + // Custom rules for Express/Node.js API + 'no-unused-vars': 'off', // Handled by TypeScript ESLint + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + 'prefer-const': 'error', + 'prefer-template': 'error', + 'no-var': 'error' + } + }, + // Specific config for test files + { + files: ['**/*.test.{ts,js}', '**/*.spec.{ts,js}'], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest + } + }, + rules: { + // Allow console in tests + 'no-console': 'off', + // Allow any in tests for mocking + '@typescript-eslint/no-explicit-any': 'off' + } + } +) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package-lock.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package-lock.json new file mode 100644 index 000000000..65e465f66 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package-lock.json @@ -0,0 +1,5768 @@ +{ + "name": "typescript-api-project", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript-api-project", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/cors": "^2.8.19", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "better-sqlite3": "^12.4.1", + "cors": "^2.8.5", + "express": "^4.21.2", + "globals": "16.4.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@types/express": "^5.0.3", + "@types/node": "^24.7.2", + "@types/supertest": "^6.0.3", + "@vitest/coverage-v8": "^4.0.10", + "eslint": "^9.37.0", + "supertest": "^7.1.4", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "typescript-eslint": "8.46.1", + "vitest": "^4.0.10" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.14.tgz", + "integrity": "sha512-EYHLqN/BY6b47qHH7gtMxAg++saoGmsjWmAq9MlXxAz4M0NcHh9iOyKhBZyU4yxZqOd8Xnqp80/5saeitz4Cng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.14", + "ast-v8-to-istanbul": "^0.3.8", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.14", + "vitest": "4.0.14" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.14.tgz", + "integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.14.tgz", + "integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.14.tgz", + "integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.14", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.14.tgz", + "integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.14", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.14.tgz", + "integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.14.tgz", + "integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.14", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", + "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", + "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vitest": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.14.tgz", + "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.14", + "@vitest/mocker": "4.0.14", + "@vitest/pretty-format": "4.0.14", + "@vitest/runner": "4.0.14", + "@vitest/snapshot": "4.0.14", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.14", + "@vitest/browser-preview": "4.0.14", + "@vitest/browser-webdriverio": "4.0.14", + "@vitest/ui": "4.0.14", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.14.tgz", + "integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.14", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package.json new file mode 100644 index 000000000..d20dc8744 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/package.json @@ -0,0 +1,47 @@ +{ + "name": "typescript-api-project", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "npm run start:install", + "start:install": "npm run db:seed && node dist/index.js", + "dev": "npm run db:seed:dev && tsx src/index.ts", + "test": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "db:init": "node dist/init-db.js", + "db:init:dev": "tsx src/init-db.ts", + "db:seed": "npm run db:init -- --seed", + "db:seed:dev": "npm run db:init:dev -- --seed", + "db:migrate": "node dist/init-db.js", + "swagger:generate": "tsx src/generate-swagger.ts" + }, + "dependencies": { + "@types/cors": "^2.8.19", + "@types/better-sqlite3": "^7.6.13", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "better-sqlite3": "^12.4.1", + "cors": "^2.8.5", + "express": "^4.21.2", + "globals": "16.4.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@types/express": "^5.0.3", + "@types/node": "^24.7.2", + "@types/supertest": "^6.0.3", + "@vitest/coverage-v8": "^4.0.10", + "eslint": "^9.37.0", + "supertest": "^7.1.4", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "typescript-eslint": "8.46.1", + "vitest": "^4.0.10" + }, + "license": "MIT" +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/config.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/config.ts new file mode 100644 index 000000000..133e2855e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/config.ts @@ -0,0 +1,26 @@ +/** + * Database configuration settings + */ + +export const DB_CONFIG = { + // Database file path - defaults to './data/app.db' but can be overridden by environment + DB_FILE: process.env.DB_FILE || './data/app.db', + + // Database engine - currently supports SQLite + DB_ENGINE: process.env.DB_ENGINE || 'sqlite', + + // Enable WAL mode for better concurrency + ENABLE_WAL: process.env.DB_ENABLE_WAL !== 'false', + + // Connection timeout in milliseconds + TIMEOUT: parseInt(process.env.DB_TIMEOUT || '30000'), + + // Enable foreign key constraints + FOREIGN_KEYS: process.env.DB_FOREIGN_KEYS !== 'false', +}; + +// Test database configuration for unit tests +export const TEST_DB_CONFIG = { + ...DB_CONFIG, + DB_FILE: ':memory:', // Use in-memory database for tests +}; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/migrate.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/migrate.ts new file mode 100644 index 000000000..67b8025ac --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/migrate.ts @@ -0,0 +1,182 @@ +/** + * Database migration runner + */ + +import fs from 'fs'; +import path from 'path'; +import { getDatabase, DatabaseConnection } from './sqlite'; + +interface Migration { + version: number; + filename: string; + sql: string; +} + +export class MigrationRunner { + private db: DatabaseConnection; + private migrationsDir: string; + + constructor(db: DatabaseConnection, migrationsDir: string = '../database/migrations') { + this.db = db; + this.migrationsDir = path.resolve(migrationsDir); + } + + /** + * Initialize the migrations table + */ + private async initializeMigrationsTable(): Promise { + const sql = ` + CREATE TABLE IF NOT EXISTS migrations ( + version INTEGER PRIMARY KEY, + filename TEXT NOT NULL, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `; + + await this.db.run(sql); + } + + /** + * Get all migration files from the migrations directory + */ + private getMigrationFiles(): Migration[] { + if (!fs.existsSync(this.migrationsDir)) { + throw new Error(`Migrations directory not found: ${this.migrationsDir}`); + } + + const files = fs + .readdirSync(this.migrationsDir) + .filter((file) => file.endsWith('.sql')) + .sort(); + + return files.map((filename) => { + const version = this.extractVersionFromFilename(filename); + const filePath = path.join(this.migrationsDir, filename); + const sql = fs.readFileSync(filePath, 'utf-8'); + + return { + version, + filename, + sql, + }; + }); + } + + /** + * Extract version number from migration filename + * Expected format: 001_description.sql + */ + private extractVersionFromFilename(filename: string): number { + const match = filename.match(/^(\d+)_/); + if (!match) { + throw new Error( + `Invalid migration filename format: ${filename}. Expected format: 001_description.sql`, + ); + } + return parseInt(match[1], 10); + } + + /** + * Get applied migrations from the database + */ + private async getAppliedMigrations(): Promise { + const rows = await this.db.all<{ version: number }>( + 'SELECT version FROM migrations ORDER BY version', + ); + return rows.map((row) => row.version); + } + + /** + * Apply a single migration + */ + private async applyMigration(migration: Migration): Promise { + console.log(`Applying migration ${migration.version}: ${migration.filename}`); + + try { + // Split SQL into individual statements and execute each + const statements = migration.sql + .split(';') + .map((stmt) => stmt.trim()) + .filter((stmt) => stmt.length > 0); + + for (const statement of statements) { + if (statement.trim()) { + await this.db.run(statement); + } + } + + // Record the migration as applied + await this.db.run('INSERT INTO migrations (version, filename) VALUES (?, ?)', [ + migration.version, + migration.filename, + ]); + + console.log(`✅ Migration ${migration.version} applied successfully`); + } catch (error) { + console.error(`❌ Failed to apply migration ${migration.version}: ${error}`); + throw error; + } + } + + /** + * Run all pending migrations + */ + public async runMigrations(): Promise { + console.log('🚀 Starting database migration...'); + + try { + // Initialize migrations table + await this.initializeMigrationsTable(); + + // Get all migrations and applied migrations + const availableMigrations = this.getMigrationFiles(); + const appliedMigrations = await this.getAppliedMigrations(); + + // Find pending migrations + const pendingMigrations = availableMigrations.filter( + (migration) => !appliedMigrations.includes(migration.version), + ); + + if (pendingMigrations.length === 0) { + console.log('✅ No pending migrations. Database is up to date.'); + return; + } + + console.log(`📋 Found ${pendingMigrations.length} pending migration(s)`); + + // Apply each pending migration + for (const migration of pendingMigrations) { + await this.applyMigration(migration); + } + + console.log('🎉 All migrations completed successfully!'); + } catch (error) { + console.error('💥 Migration failed:', error); + throw error; + } + } + + /** + * Get the current database version + */ + public async getCurrentVersion(): Promise { + await this.initializeMigrationsTable(); + + const result = await this.db.get<{ version: number }>( + 'SELECT MAX(version) as version FROM migrations', + ); + + return result?.version || 0; + } +} + +/** + * Run migrations using the global database connection + */ +export async function runMigrations(isTest: boolean = false): Promise { + const db = await getDatabase(isTest); + const migrationsDir = path.join(__dirname, '../../../database/migrations'); + const runner = new MigrationRunner(db, migrationsDir); + + await runner.runMigrations(); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/seed.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/seed.ts new file mode 100644 index 000000000..70c3346e8 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/seed.ts @@ -0,0 +1,166 @@ +/** + * Database seeder for populating initial data + */ + +import fs from 'fs'; +import path from 'path'; +import { getDatabase, DatabaseConnection } from './sqlite'; + +export class DatabaseSeeder { + private db: DatabaseConnection; + private seedsDir: string; + + constructor(db: DatabaseConnection, seedsDir: string = '../database/seed') { + this.db = db; + this.seedsDir = path.resolve(seedsDir); + } + + /** + * Check if database has been seeded + */ + private async isSeeded(): Promise { + try { + // Check if any suppliers exist (assuming suppliers are seeded first) + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM suppliers', + ); + return (result?.count || 0) > 0; + } catch { + // If table doesn't exist, database is not seeded + return false; + } + } + + /** + * Get all seed files from the seeds directory + */ + private getSeedFiles(): string[] { + if (!fs.existsSync(this.seedsDir)) { + console.log(`Seeds directory not found: ${this.seedsDir}. Skipping seeding.`); + return []; + } + + return fs + .readdirSync(this.seedsDir) + .filter((file) => file.endsWith('.sql')) + .sort(); // Ensure predictable order + } + + /** + * Execute a seed file + */ + private async executeSeedFile(filename: string): Promise { + const filePath = path.join(this.seedsDir, filename); + const sql = fs.readFileSync(filePath, 'utf-8'); + + console.log(`🌱 Seeding from: ${filename}`); + + try { + // Split SQL into individual statements and execute each + const statements = sql + .split(';') + .map((stmt) => stmt.trim()) + .filter((stmt) => stmt.length > 0); + + for (const statement of statements) { + if (statement.trim()) { + await this.db.run(statement); + } + } + + console.log(`✅ Seeded successfully: ${filename}`); + } catch (error) { + console.error(`❌ Failed to seed ${filename}:`, error); + throw error; + } + } + + /** + * Seed the database with initial data + */ + public async seedDatabase(force: boolean = false): Promise { + console.log('🌱 Starting database seeding...'); + + try { + // Check if already seeded + if (!force && (await this.isSeeded())) { + console.log('✅ Database already seeded. Use force=true to re-seed.'); + return; + } + + // Get all seed files + const seedFiles = this.getSeedFiles(); + + if (seedFiles.length === 0) { + console.log('📝 No seed files found. Skipping seeding.'); + return; + } + + console.log(`📋 Found ${seedFiles.length} seed file(s)`); + + // Execute each seed file + for (const filename of seedFiles) { + await this.executeSeedFile(filename); + } + + console.log('🎉 Database seeding completed successfully!'); + } catch (error) { + console.error('💥 Seeding failed:', error); + throw error; + } + } + + /** + * Clear all data from the database (useful for re-seeding) + */ + public async clearDatabase(): Promise { + console.log('🧹 Clearing database...'); + + try { + // Get all table names + const tables = await this.db.all<{ name: string }>( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'migrations'", + ); + + // Disable foreign key constraints temporarily + await this.db.run('PRAGMA foreign_keys = OFF'); + + // Clear all tables + for (const table of tables) { + await this.db.run(`DELETE FROM ${table.name}`); + console.log(`🗑️ Cleared table: ${table.name}`); + } + + // Re-enable foreign key constraints + await this.db.run('PRAGMA foreign_keys = ON'); + + console.log('✅ Database cleared successfully'); + } catch (error) { + console.error('❌ Failed to clear database:', error); + throw error; + } + } +} + +/** + * Seed the database using the global database connection + */ +export async function seedDatabase(force: boolean = false, isTest: boolean = false): Promise { + const db = await getDatabase(isTest); + const seedsDir = path.join(__dirname, '../../../database/seed'); + const seeder = new DatabaseSeeder(db, seedsDir); + + await seeder.seedDatabase(force); +} + +/** + * Clear and re-seed the database + */ +export async function reseedDatabase(isTest: boolean = false): Promise { + const db = await getDatabase(isTest); + const seedsDir = path.join(__dirname, '../../../database/seed'); + const seeder = new DatabaseSeeder(db, seedsDir); + + await seeder.clearDatabase(); + await seeder.seedDatabase(true); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/sqlite.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/sqlite.ts new file mode 100644 index 000000000..c44bcdce9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/db/sqlite.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * SQLite database connection and helper functions + */ + +import Database from 'better-sqlite3'; +import fs from 'fs'; +import path from 'path'; +import { DB_CONFIG, TEST_DB_CONFIG } from './config'; + +/* + * Database connection and query execution + * Wraps better-sqlite3 Database to maintain API compatibility + */ +class DatabaseConnection { + public db: Database.Database; + + constructor(db: Database.Database) { + this.db = db; + } + + /** + * Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE) + * @param sql SQL statement to execute + * @param params Parameters to bind to the SQL statement + * @returns Object containing lastID (for INSERT) and number of changes + * @throws Error if lastInsertRowid exceeds JavaScript's safe integer range + * @note better-sqlite3 returns lastInsertRowid as number | bigint. We convert bigint to number + * and validate it's within safe integer range to prevent silent truncation. + */ + public run(sql: string, params: unknown[] = []): Promise<{ lastID?: number; changes: number }> { + const info = this.db.prepare(sql).run(...(params as any[])); + const lastID = info.lastInsertRowid; + + // Convert bigint to number with safety check to prevent silent truncation + if (typeof lastID === 'bigint') { + if (lastID > Number.MAX_SAFE_INTEGER || lastID < Number.MIN_SAFE_INTEGER) { + throw new Error(`Row ID ${lastID} exceeds safe integer range (${Number.MIN_SAFE_INTEGER} to ${Number.MAX_SAFE_INTEGER})`); + } + return Promise.resolve({ lastID: Number(lastID), changes: info.changes }); + } + + return Promise.resolve({ lastID, changes: info.changes }); + } + + public get(sql: string, params: unknown[] = []): Promise { + const row = this.db.prepare(sql).get(...(params as any[])); + return Promise.resolve(row as T | undefined); + } + + public all(sql: string, params: unknown[] = []): Promise { + const rows = this.db.prepare(sql).all(...(params as any[])); + return Promise.resolve(rows as T[]); + } + + public close(): Promise { + this.db.close(); + return Promise.resolve(); + } +} + +class SQLiteHelper { + private static instance: SQLiteHelper; + private connection: Database.Database | null = null; + + private constructor() {} + + public static getInstance(): SQLiteHelper { + if (!SQLiteHelper.instance) { + SQLiteHelper.instance = new SQLiteHelper(); + } + return SQLiteHelper.instance; + } + + /** + * Initialize database connection + */ + public async connect(isTest: boolean = false): Promise { + const config = isTest ? TEST_DB_CONFIG : DB_CONFIG; + + // Ensure data directory exists for file-based databases + if (config.DB_FILE !== ':memory:') { + const dataDir = path.dirname(config.DB_FILE); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + } + + try { + // better-sqlite3 constructor options + const options: Database.Options = { + verbose: process.env.NODE_ENV === 'development' ? console.log : undefined, + timeout: config.TIMEOUT + }; + + this.connection = new Database(config.DB_FILE, options); + + // Configure database settings + this.setupDatabase(config); + + const wrappedConnection = this.wrapConnection(this.connection); + return wrappedConnection; + } catch (err) { + throw new Error(`Failed to connect to database: ${(err as Error).message}`); + } + } + + /** + * Configure database settings + */ + private setupDatabase(config: typeof DB_CONFIG): void { + if (!this.connection) return; + + // Enable foreign key constraints + if (config.FOREIGN_KEYS) { + try { + this.connection.pragma('foreign_keys = ON'); + } catch (err) { + throw new Error(`Failed to enable foreign key constraints: ${(err as Error).message}`); + } + } + + // Enable WAL mode for better concurrency (only for file databases) + if (config.ENABLE_WAL && config.DB_FILE !== ':memory:') { + try { + this.connection.pragma('journal_mode = WAL'); + } catch (err) { + throw new Error(`Failed to enable WAL mode: ${(err as Error).message}`); + } + } + } + + /** + * Wrap the database connection with promisified methods + */ + private wrapConnection(db: Database.Database): DatabaseConnection { + return new DatabaseConnection(db); + } + + /** + * Close database connection + */ + public async close(): Promise { + if (this.connection) { + try { + this.connection.close(); + this.connection = null; + } catch (err) { + throw new Error(`Failed to close database: ${(err as Error).message}`); + } + } + } +} + +// Global database connection instance +let dbConnection: DatabaseConnection | null = null; + +/** + * Get the global database connection + */ +export async function getDatabase(isTest: boolean = false): Promise { + if (!dbConnection) { + const helper = SQLiteHelper.getInstance(); + dbConnection = await helper.connect(isTest); + } + return dbConnection; +} + +/** + * Close the global database connection + */ +export async function closeDatabase(): Promise { + if (dbConnection) { + await dbConnection.close(); + dbConnection = null; + } +} + +export { SQLiteHelper, DatabaseConnection }; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/generate-swagger.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/generate-swagger.ts new file mode 100644 index 000000000..3a968bf1d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/generate-swagger.ts @@ -0,0 +1,27 @@ +import swaggerJsdoc from 'swagger-jsdoc'; +import { writeFileSync } from 'fs'; +import { resolve } from 'path'; + +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'Express API with Swagger', + version: '1.0.0', + description: 'REST API documentation using Swagger/OpenAPI', + }, + servers: [ + { + url: 'http://localhost:3000', + description: 'Development server', + }, + ], + }, + apis: ['./src/models/*.ts', './src/routes/*.ts'], +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); +const outputPath = resolve(__dirname, '..', 'api-swagger.json'); + +writeFileSync(outputPath, JSON.stringify(swaggerSpec, null, 4) + '\n'); +console.log(`Swagger spec written to ${outputPath}`); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/index.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/index.ts new file mode 100644 index 000000000..d2f278a6b --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/index.ts @@ -0,0 +1,108 @@ +import express from 'express'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; +import cors from 'cors'; +import deliveryRoutes from './routes/delivery'; +import orderDetailDeliveryRoutes from './routes/orderDetailDelivery'; +import productRoutes from './routes/product'; +import orderDetailRoutes from './routes/orderDetail'; +import orderRoutes from './routes/order'; +import branchRoutes from './routes/branch'; +import headquartersRoutes from './routes/headquarters'; +import supplierRoutes from './routes/supplier'; +import { initializeDatabase } from './init-db'; +import { errorHandler } from './utils/errors'; + +const app = express(); +const port = process.env.PORT || 3000; + +// Parse CORS origins from environment variable if available +const corsOrigins = process.env.API_CORS_ORIGINS + ? process.env.API_CORS_ORIGINS.split(',') + : [ + 'http://localhost:5137', + 'http://127.0.0.1:5137', + // Allow all Codespace domains + /^https:\/\/.*\.app\.github\.dev$/, + // Allow all Azure Container Apps domains + /^https:\/\/.*\.azurecontainerapps\.io$/, + ]; + +console.log('Configured CORS origins:', corsOrigins); + +// Enable CORS for the frontend +app.use( + cors({ + origin: corsOrigins, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + credentials: true, + }), +); + +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'Express API with Swagger', + version: '1.0.0', + description: 'REST API documentation using Swagger/OpenAPI', + }, + servers: [ + { + url: `http://localhost:${port}`, + description: 'Development server (HTTP)', + }, + { + url: `https://localhost:${port}`, + description: 'Development server (HTTPS)', + }, + ], + }, + apis: ['./src/models/*.ts', './src/routes/*.ts'], +}; + +const swaggerDocs = swaggerJsdoc(swaggerOptions); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs)); + +app.get('/api-docs.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerDocs); +}); + +app.use(express.json()); + +app.use('/api/deliveries', deliveryRoutes); +app.use('/api/order-detail-deliveries', orderDetailDeliveryRoutes); +app.use('/api/products', productRoutes); +app.use('/api/order-details', orderDetailRoutes); +app.use('/api/orders', orderRoutes); +app.use('/api/branches', branchRoutes); +app.use('/api/headquarters', headquartersRoutes); +app.use('/api/suppliers', supplierRoutes); + +app.get('/', (req, res) => { + res.send('Hello, world!'); +}); + +// Add error handling middleware +app.use(errorHandler); + +// Initialize database and start server +async function startServer() { + try { + console.log('🚀 Initializing database...'); + await initializeDatabase(true); // Always attempt seeding - the seeder checks if it's needed + console.log('✅ Database initialized successfully'); + + app.listen(port, () => { + console.log(`Server is running on port ${port}`); + console.log(`API documentation is available at http://localhost:${port}/api-docs`); + }); + } catch (error) { + console.error('❌ Failed to start server:', error); + process.exit(1); + } +} + +startServer(); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/init-db.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/init-db.ts new file mode 100644 index 000000000..a6b0b8d9d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/init-db.ts @@ -0,0 +1,46 @@ +/** + * Database initialization script + * This script runs migrations and optionally seeds the database + */ + +import { runMigrations } from './db/migrate'; +import { seedDatabase } from './db/seed'; +import { closeDatabase } from './db/sqlite'; + +async function initializeDatabase(shouldSeed: boolean = true): Promise { + console.log('🚀 Initializing database...'); + + try { + // Run migrations + await runMigrations(); + + // Seed database if requested + if (shouldSeed) { + await seedDatabase(); + } + + console.log('✅ Database initialization complete!'); + } catch (error) { + console.error('❌ Database initialization failed:', error); + throw error; + } finally { + await closeDatabase(); + } +} + +// Run if called directly +if (require.main === module) { + const shouldSeed = process.argv.includes('--seed') || process.argv.includes('-s'); + + initializeDatabase(shouldSeed) + .then(() => { + console.log('Database initialization completed successfully'); + process.exit(0); + }) + .catch((error) => { + console.error('Database initialization failed:', error); + process.exit(1); + }); +} + +export { initializeDatabase }; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/branch.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/branch.ts new file mode 100644 index 000000000..e7c385c48 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/branch.ts @@ -0,0 +1,47 @@ +/** + * @swagger + * components: + * schemas: + * Branch: + * type: object + * required: + * - branchId + * - name + * - headquartersId + * properties: + * branchId: + * type: integer + * description: The unique identifier for the branch + * headquartersId: + * type: integer + * description: The ID of the headquarters this branch belongs to + * name: + * type: string + * description: The name of the branch + * address: + * type: string + * description: Physical address of the branch + * phone: + * type: string + * description: Contact phone number for the branch + * description: + * type: string + * description: Additional details about the branch + * contactPerson: + * type: string + * description: Name of the primary contact person + * email: + * type: string + * format: email + * description: Contact email for the branch + */ +export interface Branch { + branchId: number; + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/delivery.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/delivery.ts new file mode 100644 index 000000000..fb58ce14e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/delivery.ts @@ -0,0 +1,38 @@ +/** + * @swagger + * components: + * schemas: + * Delivery: + * type: object + * required: + * - deliveryId + * - supplierId + * - status + * properties: + * deliveryId: + * type: integer + * description: The unique identifier for the delivery + * supplierId: + * type: integer + * description: The ID of the supplier for this delivery + * deliveryDate: + * type: string + * description: The date of the delivery + * name: + * type: string + * description: The name of the delivery + * description: + * type: string + * description: Additional details about the delivery + * status: + * type: string + * description: Current status of the delivery + */ +export interface Delivery { + deliveryId: number; + supplierId: number; + deliveryDate: string; + name: string; + description: string; + status: string; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/headquarters.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/headquarters.ts new file mode 100644 index 000000000..700214887 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/headquarters.ts @@ -0,0 +1,58 @@ +/** + * @swagger + * components: + * schemas: + * Headquarters: + * type: object + * required: + * - headquartersId + * - name + * properties: + * headquartersId: + * type: integer + * description: The unique identifier for the headquarters + * name: + * type: string + * description: The name of the headquarters + * address: + * type: string + * description: Main office address of the headquarters + * phone: + * type: string + * description: Contact phone number for the headquarters + * description: + * type: string + * description: Additional details about the headquarters + * contactPerson: + * type: string + * description: Name of the primary contact person + * email: + * type: string + * format: email + * description: Contact email for the headquarters + * city: + * type: string + * description: City where the headquarters is located + * country: + * type: string + * description: Country where the headquarters is located + * floorCount: + * type: integer + * description: Number of floors in the headquarters building + * capacity: + * type: integer + * description: Total capacity of the headquarters + */ +export interface Headquarters { + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; + city?: string; + country?: string; + floorCount?: number; + capacity?: number; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/order.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/order.ts new file mode 100644 index 000000000..7938253fa --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/order.ts @@ -0,0 +1,40 @@ +/** + * @swagger + * components: + * schemas: + * Order: + * type: object + * required: + * - orderId + * - branchId + * - orderDate + * properties: + * orderId: + * type: integer + * description: The unique identifier for the order + * branchId: + * type: integer + * description: The ID of the branch that placed the order + * orderDate: + * type: string + * format: date-time + * description: The date and time when the order was placed + * name: + * type: string + * description: The name of the order + * description: + * type: string + * description: Additional details about the order + * status: + * type: string + * description: The current status of the order + * enum: [pending, processing, shipped, delivered, cancelled] + */ +export interface Order { + orderId: number; + branchId: number; + orderDate: string; + name: string; + description: string; + status: string; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetail.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetail.ts new file mode 100644 index 000000000..4c2179b43 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetail.ts @@ -0,0 +1,41 @@ +/** + * @swagger + * components: + * schemas: + * OrderDetail: + * type: object + * required: + * - orderDetailId + * - orderId + * - productId + * - quantity + * - unitPrice + * properties: + * orderDetailId: + * type: integer + * description: The unique identifier for the order detail + * orderId: + * type: integer + * description: The ID of the parent order + * productId: + * type: integer + * description: The ID of the product ordered + * quantity: + * type: integer + * description: The quantity of products ordered + * unitPrice: + * type: number + * format: float + * description: The price per unit + * notes: + * type: string + * description: Additional notes for the order detail + */ +export interface OrderDetail { + orderDetailId: number; + orderId: number; + productId: number; + quantity: number; + unitPrice: number; + notes: string; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetailDelivery.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetailDelivery.ts new file mode 100644 index 000000000..8a1cb2dfc --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/orderDetailDelivery.ts @@ -0,0 +1,34 @@ +/** + * @swagger + * components: + * schemas: + * OrderDetailDelivery: + * type: object + * required: + * - orderDetailDeliveryId + * - orderDetailId + * - deliveryId + * properties: + * orderDetailDeliveryId: + * type: integer + * description: The unique identifier for the order detail delivery + * orderDetailId: + * type: integer + * description: The ID of the related order detail + * deliveryId: + * type: integer + * description: The ID of the related delivery + * quantity: + * type: integer + * description: The quantity of items in this delivery + * notes: + * type: string + * description: Additional notes about this delivery + */ +export interface OrderDetailDelivery { + orderDetailDeliveryId: number; + orderDetailId: number; + deliveryId: number; + quantity: number; + notes: string; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/product.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/product.ts new file mode 100644 index 000000000..f141f4aba --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/product.ts @@ -0,0 +1,55 @@ +/** + * @swagger + * components: + * schemas: + * Product: + * type: object + * required: + * - productId + * - supplierId + * - name + * - price + * - sku + * - unit + * properties: + * productId: + * type: integer + * description: The unique identifier for the product + * supplierId: + * type: integer + * description: The ID of the supplier providing this product + * name: + * type: string + * description: The name of the product + * description: + * type: string + * description: Detailed description of the product + * price: + * type: number + * format: float + * description: The current price of the product + * sku: + * type: string + * description: Stock keeping unit code for the product + * unit: + * type: string + * description: Unit of measure for the product (e.g., "piece") + * imgName: + * type: string + * description: Filename of the product image + * discount: + * type: number + * format: float + * description: Discount percentage (if applicable) expressed as a decimal (e.g., 0.25 for 25%) + */ +export interface Product { + productId: number; + supplierId: number; + name: string; + description: string; + price: number; + sku: string; + unit: string; + imgName: string; + discount?: number; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/supplier.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/supplier.ts new file mode 100644 index 000000000..2875c9a13 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/models/supplier.ts @@ -0,0 +1,46 @@ +/** + * @swagger + * components: + * schemas: + * Supplier: + * type: object + * required: + * - supplierId + * - name + * properties: + * supplierId: + * type: integer + * description: The unique identifier for the supplier + * name: + * type: string + * description: The name of the supplier + * contactPerson: + * type: string + * description: Name of the primary contact person + * email: + * type: string + * format: email + * description: Contact email for the supplier + * phone: + * type: string + * description: Contact phone number for the supplier + * description: + * type: string + * description: Additional details about the supplier + * active: + * type: boolean + * description: Whether the supplier is active + * verified: + * type: boolean + * description: Whether the supplier is verified + */ +export interface Supplier { + supplierId: number; + name: string; + description: string; + contactPerson: string; + email: string; + phone: string; + active: boolean; + verified: boolean; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/branchesRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/branchesRepo.ts new file mode 100644 index 000000000..db3e6d7ae --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/branchesRepo.ts @@ -0,0 +1,165 @@ +/** + * Repository for branches data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Branch } from '../models/branch'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class BranchesRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all branches + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM branches ORDER BY branch_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get branch by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM branches WHERE branch_id = ?', [id]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new branch + */ + async create(branch: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('branches', branch); + const result = await this.db.run(sql, values); + + const createdBranch = await this.findById(result.lastID || 0); + if (!createdBranch) { + throw new Error('Failed to retrieve created branch'); + } + + return createdBranch; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update branch by ID + */ + async update(id: number, branch: Partial>): Promise { + try { + const { sql, values } = buildUpdateSQL('branches', branch, 'branch_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Branch', id); + } + + const updatedBranch = await this.findById(id); + if (!updatedBranch) { + throw new Error('Failed to retrieve updated branch'); + } + + return updatedBranch; + } catch (error) { + handleDatabaseError(error, 'Branch', id); + } + } + + /** + * Delete branch by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM branches WHERE branch_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Branch', id); + } + } catch (error) { + handleDatabaseError(error, 'Branch', id); + } + } + + /** + * Check if branch exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM branches WHERE branch_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find branches by headquarters ID + */ + async findByHeadquartersId(headquartersId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM branches WHERE headquarters_id = ? ORDER BY name', + [headquartersId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find branches by name (partial match) + */ + async findByName(name: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM branches WHERE name LIKE ? ORDER BY name', + [`%${name}%`], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createBranchesRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new BranchesRepository(db); +} + +// Singleton instance for default usage +let branchesRepo: BranchesRepository | null = null; + +export async function getBranchesRepository(isTest: boolean = false): Promise { + const isTestEnv = isTest || process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'; + if (isTestEnv) { + // In tests, always return a fresh repository bound to the current in-memory DB + return createBranchesRepository(true); + } + if (!branchesRepo) { + branchesRepo = await createBranchesRepository(false); + } + return branchesRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/deliveriesRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/deliveriesRepo.ts new file mode 100644 index 000000000..b6ba66991 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/deliveriesRepo.ts @@ -0,0 +1,192 @@ +/** + * Repository for deliveries data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Delivery } from '../models/delivery'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class DeliveriesRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all deliveries + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM deliveries ORDER BY delivery_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get delivery by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM deliveries WHERE delivery_id = ?', [id]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new delivery + */ + async create(delivery: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('deliveries', delivery); + const result = await this.db.run(sql, values); + + const createdDelivery = await this.findById(result.lastID || 0); + if (!createdDelivery) { + throw new Error('Failed to retrieve created delivery'); + } + + return createdDelivery; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update delivery by ID + */ + async update(id: number, delivery: Partial>): Promise { + try { + const { sql, values } = buildUpdateSQL('deliveries', delivery, 'delivery_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Delivery', id); + } + + const updatedDelivery = await this.findById(id); + if (!updatedDelivery) { + throw new Error('Failed to retrieve updated delivery'); + } + + return updatedDelivery; + } catch (error) { + handleDatabaseError(error, 'Delivery', id); + } + } + + /** + * Delete delivery by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM deliveries WHERE delivery_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Delivery', id); + } + } catch (error) { + handleDatabaseError(error, 'Delivery', id); + } + } + + /** + * Check if delivery exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM deliveries WHERE delivery_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find deliveries by supplier ID + */ + async findBySupplierId(supplierId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM deliveries WHERE supplier_id = ? ORDER BY delivery_date DESC', + [supplierId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find deliveries by status + */ + async findByStatus(status: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM deliveries WHERE status = ? ORDER BY delivery_date DESC', + [status], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find deliveries by date range + */ + async findByDateRange(startDate: string, endDate: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM deliveries WHERE delivery_date >= ? AND delivery_date <= ? ORDER BY delivery_date DESC', + [startDate, endDate], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update delivery status + */ + async updateStatus(id: number, status: string): Promise { + try { + return await this.update(id, { status }); + } catch (error) { + handleDatabaseError(error, 'Delivery', id); + } + } +} + +// Factory function to create repository instance +export async function createDeliveriesRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new DeliveriesRepository(db); +} + +// Singleton instance for default usage +let deliveriesRepo: DeliveriesRepository | null = null; + +export async function getDeliveriesRepository( + isTest: boolean = false, +): Promise { + const isTestEnv = isTest || process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'; + if (isTestEnv) { + return createDeliveriesRepository(true); + } + if (!deliveriesRepo) { + deliveriesRepo = await createDeliveriesRepository(false); + } + return deliveriesRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/headquartersRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/headquartersRepo.ts new file mode 100644 index 000000000..4d60cc723 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/headquartersRepo.ts @@ -0,0 +1,152 @@ +/** + * Repository for headquarters data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Headquarters } from '../models/headquarters'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class HeadquartersRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all headquarters + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM headquarters ORDER BY headquarters_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get headquarters by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM headquarters WHERE headquarters_id = ?', [ + id, + ]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new headquarters + */ + async create(headquarters: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('headquarters', headquarters); + const result = await this.db.run(sql, values); + + const createdHeadquarters = await this.findById(result.lastID || 0); + if (!createdHeadquarters) { + throw new Error('Failed to retrieve created headquarters'); + } + + return createdHeadquarters; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update headquarters by ID + */ + async update( + id: number, + headquarters: Partial>, + ): Promise { + try { + const { sql, values } = buildUpdateSQL('headquarters', headquarters, 'headquarters_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Headquarters', id); + } + + const updatedHeadquarters = await this.findById(id); + if (!updatedHeadquarters) { + throw new Error('Failed to retrieve updated headquarters'); + } + + return updatedHeadquarters; + } catch (error) { + handleDatabaseError(error, 'Headquarters', id); + } + } + + /** + * Delete headquarters by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM headquarters WHERE headquarters_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Headquarters', id); + } + } catch (error) { + handleDatabaseError(error, 'Headquarters', id); + } + } + + /** + * Check if headquarters exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM headquarters WHERE headquarters_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find headquarters by name (partial match) + */ + async findByName(name: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM headquarters WHERE name LIKE ? ORDER BY name', + [`%${name}%`], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createHeadquartersRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new HeadquartersRepository(db); +} + +// Singleton instance for default usage +let headquartersRepo: HeadquartersRepository | null = null; + +export async function getHeadquartersRepository( + isTest: boolean = false, +): Promise { + if (!headquartersRepo) { + headquartersRepo = await createHeadquartersRepository(isTest); + } + return headquartersRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailDeliveriesRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailDeliveriesRepo.ts new file mode 100644 index 000000000..26cbaa9bf --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailDeliveriesRepo.ts @@ -0,0 +1,194 @@ +/** + * Repository for order detail deliveries data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { OrderDetailDelivery } from '../models/orderDetailDelivery'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class OrderDetailDeliveriesRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all order detail deliveries + */ + async findAll(): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM order_detail_deliveries ORDER BY order_detail_delivery_id', + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get order detail delivery by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get( + 'SELECT * FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', + [id], + ); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new order detail delivery + */ + async create( + orderDetailDelivery: Omit, + ): Promise { + try { + const { sql, values } = buildInsertSQL('order_detail_deliveries', orderDetailDelivery); + const result = await this.db.run(sql, values); + + const createdOrderDetailDelivery = await this.findById(result.lastID || 0); + if (!createdOrderDetailDelivery) { + throw new Error('Failed to retrieve created order detail delivery'); + } + + return createdOrderDetailDelivery; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update order detail delivery by ID + */ + async update( + id: number, + orderDetailDelivery: Partial>, + ): Promise { + try { + const { sql, values } = buildUpdateSQL( + 'order_detail_deliveries', + orderDetailDelivery, + 'order_detail_delivery_id = ?', + ); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('OrderDetailDelivery', id); + } + + const updatedOrderDetailDelivery = await this.findById(id); + if (!updatedOrderDetailDelivery) { + throw new Error('Failed to retrieve updated order detail delivery'); + } + + return updatedOrderDetailDelivery; + } catch (error) { + handleDatabaseError(error, 'OrderDetailDelivery', id); + } + } + + /** + * Delete order detail delivery by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run( + 'DELETE FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', + [id], + ); + + if (result.changes === 0) { + throw new NotFoundError('OrderDetailDelivery', id); + } + } catch (error) { + handleDatabaseError(error, 'OrderDetailDelivery', id); + } + } + + /** + * Check if order detail delivery exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find order detail deliveries by order detail ID + */ + async findByOrderDetailId(orderDetailId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM order_detail_deliveries WHERE order_detail_id = ? ORDER BY order_detail_delivery_id', + [orderDetailId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find order detail deliveries by delivery ID + */ + async findByDeliveryId(deliveryId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM order_detail_deliveries WHERE delivery_id = ? ORDER BY order_detail_delivery_id', + [deliveryId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get total quantity delivered for an order detail + */ + async getTotalQuantityByOrderDetailId(orderDetailId: number): Promise { + try { + const result = await this.db.get<{ total: number }>( + 'SELECT SUM(quantity) as total FROM order_detail_deliveries WHERE order_detail_id = ?', + [orderDetailId], + ); + return result?.total || 0; + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createOrderDetailDeliveriesRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new OrderDetailDeliveriesRepository(db); +} + +// Singleton instance for default usage +let orderDetailDeliveriesRepo: OrderDetailDeliveriesRepository | null = null; + +export async function getOrderDetailDeliveriesRepository( + isTest: boolean = false, +): Promise { + if (!orderDetailDeliveriesRepo) { + orderDetailDeliveriesRepo = await createOrderDetailDeliveriesRepository(isTest); + } + return orderDetailDeliveriesRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailsRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailsRepo.ts new file mode 100644 index 000000000..601df8680 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/orderDetailsRepo.ts @@ -0,0 +1,182 @@ +/** + * Repository for order details data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { OrderDetail } from '../models/orderDetail'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class OrderDetailsRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all order details + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM order_details ORDER BY order_detail_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get order detail by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM order_details WHERE order_detail_id = ?', [ + id, + ]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new order detail + */ + async create(orderDetail: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('order_details', orderDetail); + const result = await this.db.run(sql, values); + + const createdOrderDetail = await this.findById(result.lastID || 0); + if (!createdOrderDetail) { + throw new Error('Failed to retrieve created order detail'); + } + + return createdOrderDetail; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update order detail by ID + */ + async update( + id: number, + orderDetail: Partial>, + ): Promise { + try { + const { sql, values } = buildUpdateSQL('order_details', orderDetail, 'order_detail_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('OrderDetail', id); + } + + const updatedOrderDetail = await this.findById(id); + if (!updatedOrderDetail) { + throw new Error('Failed to retrieve updated order detail'); + } + + return updatedOrderDetail; + } catch (error) { + handleDatabaseError(error, 'OrderDetail', id); + } + } + + /** + * Delete order detail by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM order_details WHERE order_detail_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('OrderDetail', id); + } + } catch (error) { + handleDatabaseError(error, 'OrderDetail', id); + } + } + + /** + * Check if order detail exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM order_details WHERE order_detail_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find order details by order ID + */ + async findByOrderId(orderId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM order_details WHERE order_id = ? ORDER BY order_detail_id', + [orderId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find order details by product ID + */ + async findByProductId(productId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM order_details WHERE product_id = ? ORDER BY order_detail_id', + [productId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Calculate total value for an order + */ + async getTotalValueByOrderId(orderId: number): Promise { + try { + const result = await this.db.get<{ total: number }>( + 'SELECT SUM(quantity * unit_price) as total FROM order_details WHERE order_id = ?', + [orderId], + ); + return result?.total || 0; + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createOrderDetailsRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new OrderDetailsRepository(db); +} + +// Singleton instance for default usage +let orderDetailsRepo: OrderDetailsRepository | null = null; + +export async function getOrderDetailsRepository( + isTest: boolean = false, +): Promise { + if (!orderDetailsRepo) { + orderDetailsRepo = await createOrderDetailsRepository(isTest); + } + return orderDetailsRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/ordersRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/ordersRepo.ts new file mode 100644 index 000000000..76862cc9b --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/ordersRepo.ts @@ -0,0 +1,177 @@ +/** + * Repository for orders data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Order } from '../models/order'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class OrdersRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all orders + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM orders ORDER BY order_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get order by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM orders WHERE order_id = ?', [id]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new order + */ + async create(order: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('orders', order); + const result = await this.db.run(sql, values); + + const createdOrder = await this.findById(result.lastID || 0); + if (!createdOrder) { + throw new Error('Failed to retrieve created order'); + } + + return createdOrder; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update order by ID + */ + async update(id: number, order: Partial>): Promise { + try { + const { sql, values } = buildUpdateSQL('orders', order, 'order_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Order', id); + } + + const updatedOrder = await this.findById(id); + if (!updatedOrder) { + throw new Error('Failed to retrieve updated order'); + } + + return updatedOrder; + } catch (error) { + handleDatabaseError(error, 'Order', id); + } + } + + /** + * Delete order by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM orders WHERE order_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Order', id); + } + } catch (error) { + handleDatabaseError(error, 'Order', id); + } + } + + /** + * Check if order exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM orders WHERE order_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find orders by branch ID + */ + async findByBranchId(branchId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM orders WHERE branch_id = ? ORDER BY order_date DESC', + [branchId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find orders by status + */ + async findByStatus(status: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM orders WHERE status = ? ORDER BY order_date DESC', + [status], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find orders by date range + */ + async findByDateRange(startDate: string, endDate: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM orders WHERE order_date >= ? AND order_date <= ? ORDER BY order_date DESC', + [startDate, endDate], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createOrdersRepository(isTest: boolean = false): Promise { + const db = await getDatabase(isTest); + return new OrdersRepository(db); +} + +// Singleton instance for default usage +let ordersRepo: OrdersRepository | null = null; + +export async function getOrdersRepository(isTest: boolean = false): Promise { + const isTestEnv = isTest || process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'; + if (isTestEnv) { + return createOrdersRepository(true); + } + if (!ordersRepo) { + ordersRepo = await createOrdersRepository(false); + } + return ordersRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/productsRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/productsRepo.ts new file mode 100644 index 000000000..c8e749839 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/productsRepo.ts @@ -0,0 +1,164 @@ +/** + * Repository for products data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Product } from '../models/product'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class ProductsRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all products + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM products ORDER BY product_id'); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get product by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM products WHERE product_id = ?', [id]); + return row ? objectToCamelCase(row) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new product + */ + async create(product: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('products', product); + const result = await this.db.run(sql, values); + + const createdProduct = await this.findById(result.lastID || 0); + if (!createdProduct) { + throw new Error('Failed to retrieve created product'); + } + + return createdProduct; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update product by ID + */ + async update(id: number, product: Partial>): Promise { + try { + const { sql, values } = buildUpdateSQL('products', product, 'product_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Product', id); + } + + const updatedProduct = await this.findById(id); + if (!updatedProduct) { + throw new Error('Failed to retrieve updated product'); + } + + return updatedProduct; + } catch (error) { + handleDatabaseError(error, 'Product', id); + } + } + + /** + * Delete product by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM products WHERE product_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Product', id); + } + } catch (error) { + handleDatabaseError(error, 'Product', id); + } + } + + /** + * Check if product exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM products WHERE product_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find products by supplier ID + */ + async findBySupplierId(supplierId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM products WHERE supplier_id = ? ORDER BY name', + [supplierId], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find products by name (partial match) + */ + async findByName(name: string): Promise { + try { + const rows = await this.db.all( + `SELECT * FROM products WHERE name LIKE '%${name}%' ORDER BY name`, + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createProductsRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new ProductsRepository(db); +} + +// Singleton instance for default usage +let productsRepo: ProductsRepository | null = null; + +export async function getProductsRepository(isTest: boolean = false): Promise { + const isTestEnv = isTest || process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'; + if (isTestEnv) { + // In tests, always return a fresh repository bound to the current in-memory DB + return createProductsRepository(true); + } + if (!productsRepo) { + productsRepo = await createProductsRepository(false); + } + return productsRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.test.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.test.ts new file mode 100644 index 000000000..4a57cc195 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { SuppliersRepository } from './suppliersRepo'; +import { NotFoundError } from '../utils/errors'; + +// Mock the getDatabase function first +vi.mock('../db/sqlite', () => ({ + getDatabase: vi.fn() +})); + +// Import the mocked module +import { getDatabase } from '../db/sqlite'; + +describe('SuppliersRepository', () => { + let repository: SuppliersRepository; + let mockDb: any; + + beforeEach(() => { + // Create mock database connection + mockDb = { + db: {} as any, + run: vi.fn(), + get: vi.fn(), + all: vi.fn(), + close: vi.fn() + }; + + // Mock getDatabase to return our mock + (getDatabase as any).mockResolvedValue(mockDb); + + repository = new SuppliersRepository(mockDb); + vi.clearAllMocks(); + }); + + describe('findAll', () => { + it('should return all suppliers', async () => { + const mockResults = [ + { supplier_id: 1, name: 'Test Supplier', description: 'Test', contact_person: 'John', email: 'john@test.com', phone: '555-1234', active: true, verified: true } + ]; + mockDb.all.mockResolvedValue(mockResults); + + const result = await repository.findAll(); + + expect(mockDb.all).toHaveBeenCalledWith('SELECT * FROM suppliers ORDER BY supplier_id'); + expect(result).toHaveLength(1); + expect(result[0].supplierId).toBe(1); + expect(result[0].name).toBe('Test Supplier'); + }); + + it('should return empty array when no suppliers exist', async () => { + mockDb.all.mockResolvedValue([]); + + const result = await repository.findAll(); + + expect(result).toEqual([]); + }); + }); + + describe('findById', () => { + it('should return supplier when found', async () => { + const mockResult = { + supplier_id: 1, + name: 'Test Supplier', + description: 'Test', + contact_person: 'John', + email: 'john@test.com', + phone: '555-1234', + active: true, + verified: true + }; + mockDb.get.mockResolvedValue(mockResult); + + const result = await repository.findById(1); + + expect(mockDb.get).toHaveBeenCalledWith('SELECT * FROM suppliers WHERE supplier_id = ?', [1]); + expect(result?.supplierId).toBe(1); + expect(result?.name).toBe('Test Supplier'); + }); + + it('should return null when supplier not found', async () => { + mockDb.get.mockResolvedValue(undefined); + + const result = await repository.findById(999); + + expect(result).toBeNull(); + }); + }); + + describe('create', () => { + it('should create a new supplier and return it', async () => { + const newSupplier = { + name: 'New Supplier', + description: 'New Description', + contactPerson: 'Jane Doe', + email: 'jane@test.com', + phone: '555-5678', + active: true, + verified: false + }; + + mockDb.run.mockResolvedValue({ lastID: 2, changes: 1 }); + mockDb.get.mockResolvedValue({ + supplier_id: 2, + name: 'New Supplier', + description: 'New Description', + contact_person: 'Jane Doe', + email: 'jane@test.com', + phone: '555-5678', + active: true, + verified: false + }); + + const result = await repository.create(newSupplier); + + expect(mockDb.run).toHaveBeenCalledWith( + 'INSERT INTO suppliers (name, description, contact_person, email, phone, active, verified) VALUES (?, ?, ?, ?, ?, ?, ?)', + ['New Supplier', 'New Description', 'Jane Doe', 'jane@test.com', '555-5678', true, false] + ); + expect(result.supplierId).toBe(2); + expect(result.name).toBe('New Supplier'); + }); + }); + + describe('update', () => { + it('should update existing supplier and return updated data', async () => { + const updateData = { name: 'Updated Supplier' }; + + mockDb.run.mockResolvedValue({ changes: 1 }); + mockDb.get.mockResolvedValue({ + supplier_id: 1, + name: 'Updated Supplier', + description: 'Test', + contact_person: 'John', + email: 'john@test.com', + phone: '555-1234', + active: true, + verified: true + }); + + const result = await repository.update(1, updateData); + + expect(mockDb.run).toHaveBeenCalledWith( + 'UPDATE suppliers SET name = ? WHERE supplier_id = ?', + ['Updated Supplier', 1] + ); + expect(result.name).toBe('Updated Supplier'); + }); + + it('should throw NotFoundError when supplier does not exist', async () => { + mockDb.run.mockResolvedValue({ changes: 0 }); + + await expect(repository.update(999, { name: 'Updated' })) + .rejects.toThrow(NotFoundError); + }); + }); + + describe('delete', () => { + it('should delete existing supplier', async () => { + mockDb.run.mockResolvedValue({ changes: 1 }); + + await repository.delete(1); + + expect(mockDb.run).toHaveBeenCalledWith('DELETE FROM suppliers WHERE supplier_id = ?', [1]); + }); + + it('should throw NotFoundError when supplier does not exist', async () => { + mockDb.run.mockResolvedValue({ changes: 0 }); + + await expect(repository.delete(999)) + .rejects.toThrow(NotFoundError); + }); + }); + + describe('exists', () => { + it('should return true when supplier exists', async () => { + mockDb.get.mockResolvedValue({ count: 1 }); + + const result = await repository.exists(1); + + expect(result).toBe(true); + expect(mockDb.get).toHaveBeenCalledWith('SELECT COUNT(*) as count FROM suppliers WHERE supplier_id = ?', [1]); + }); + + it('should return false when supplier does not exist', async () => { + mockDb.get.mockResolvedValue({ count: 0 }); + + const result = await repository.exists(999); + + expect(result).toBe(false); + }); + }); + + describe('findByName', () => { + it('should return suppliers matching name pattern', async () => { + const mockResults = [ + { supplier_id: 1, name: 'Test Supplier', description: 'Test', contact_person: 'John', email: 'john@test.com', phone: '555-1234', active: true, verified: true } + ]; + mockDb.all.mockResolvedValue(mockResults); + + const result = await repository.findByName('Test'); + + expect(mockDb.all).toHaveBeenCalledWith( + 'SELECT * FROM suppliers WHERE name LIKE ? ORDER BY name', + ['%Test%'] + ); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Test Supplier'); + }); + }); +}); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.ts new file mode 100644 index 000000000..7237d4f79 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/repositories/suppliersRepo.ts @@ -0,0 +1,162 @@ +/** + * Repository for suppliers data access + */ + +import { getDatabase, DatabaseConnection } from '../db/sqlite'; +import { Supplier } from '../models/supplier'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase, mapDatabaseRows, DatabaseRow } from '../utils/sql'; + +export class SuppliersRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + /** + * Get all suppliers + */ + async findAll(): Promise { + try { + const rows = await this.db.all('SELECT * FROM suppliers ORDER BY supplier_id'); + return mapDatabaseRows(rows).map(this.convertBooleanFields); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get supplier by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get('SELECT * FROM suppliers WHERE supplier_id = ?', [id]); + return row ? this.convertBooleanFields(objectToCamelCase(row)) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Convert integer fields to boolean for SQLite compatibility + */ + private convertBooleanFields(supplier: Supplier): Supplier { + return { + ...supplier, + active: Boolean(supplier.active), + verified: Boolean(supplier.verified), + }; + } + + /** + * Create a new supplier + */ + async create(supplier: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('suppliers', supplier); + const result = await this.db.run(sql, values); + + const createdSupplier = await this.findById(result.lastID || 0); + if (!createdSupplier) { + throw new Error('Failed to retrieve created supplier'); + } + + return createdSupplier; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update supplier by ID + */ + async update(id: number, supplier: Partial>): Promise { + try { + const { sql, values } = buildUpdateSQL('suppliers', supplier, 'supplier_id = ?'); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Supplier', id); + } + + const updatedSupplier = await this.findById(id); + if (!updatedSupplier) { + throw new Error('Failed to retrieve updated supplier'); + } + + return updatedSupplier; + } catch (error) { + handleDatabaseError(error, 'Supplier', id); + } + } + + /** + * Delete supplier by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run('DELETE FROM suppliers WHERE supplier_id = ?', [id]); + + if (result.changes === 0) { + throw new NotFoundError('Supplier', id); + } + } catch (error) { + handleDatabaseError(error, 'Supplier', id); + } + } + + /** + * Check if supplier exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM suppliers WHERE supplier_id = ?', + [id], + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Find suppliers by name (partial match) + */ + async findByName(name: string): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM suppliers WHERE name LIKE ? ORDER BY name', + [`%${name}%`], + ); + return mapDatabaseRows(rows); + } catch (error) { + handleDatabaseError(error); + } + } +} + +// Factory function to create repository instance +export async function createSuppliersRepository( + isTest: boolean = false, +): Promise { + const db = await getDatabase(isTest); + return new SuppliersRepository(db); +} + +// Singleton instance for default usage +let suppliersRepo: SuppliersRepository | null = null; + +export async function getSuppliersRepository( + isTest: boolean = false, +): Promise { + const isTestEnv = isTest || process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'; + if (isTestEnv) { + return createSuppliersRepository(true); + } + if (!suppliersRepo) { + suppliersRepo = await createSuppliersRepository(false); + } + return suppliersRepo; +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.test.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.test.ts new file mode 100644 index 000000000..cd2f57a94 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; +import branchRouter from './branch'; +import { runMigrations } from '../db/migrate'; +import { closeDatabase, getDatabase } from '../db/sqlite'; +import { errorHandler } from '../utils/errors'; + +let app: express.Express; + +describe('Branch API', () => { + beforeEach(async () => { + // Ensure a fresh in-memory database for each test + await closeDatabase(); + await getDatabase(true); + await runMigrations(true); + + // Seed required foreign key: headquarters id 1 + const db = await getDatabase(); + await db.run('INSERT INTO headquarters (headquarters_id, name) VALUES (?, ?)', [1, 'HQ One']); + + // Set up express app + app = express(); + app.use(express.json()); + app.use('/branches', branchRouter); + // Attach error handler to translate repo errors + app.use(errorHandler); + }); + + afterEach(async () => { + await closeDatabase(); + }); + + it('should create a new branch', async () => { + const newBranch = { + headquartersId: 1, + name: 'Eastside Branch', + description: 'Eastern district branch', + address: '321 East St', + contactPerson: 'Emma Davis', + email: 'edavis@octo.com', + phone: '555-0203', + }; + const response = await request(app).post('/branches').send(newBranch); + expect(response.status).toBe(201); + expect(response.body).toMatchObject(newBranch); + expect(response.body.branchId).toBeDefined(); + }); + + it('should get all branches', async () => { + const response = await request(app).get('/branches'); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + it('should get a branch by ID', async () => { + // First create a branch to test getting it + const newBranch = { + headquartersId: 1, + name: 'Test Branch', + description: 'Test branch', + address: '123 Test St', + contactPerson: 'Test Person', + email: 'test@test.com', + phone: '555-0000', + }; + const createResponse = await request(app).post('/branches').send(newBranch); + const branchId = createResponse.body.branchId; + + const response = await request(app).get(`/branches/${branchId}`); + expect(response.status).toBe(200); + expect(response.body.branchId).toBe(branchId); + }); + + it('should update a branch by ID', async () => { + // First create a branch to test updating it + const newBranch = { + headquartersId: 1, + name: 'Original Branch', + description: 'Original description', + address: '123 Original St', + contactPerson: 'Original Person', + email: 'original@test.com', + phone: '555-0001', + }; + const createResponse = await request(app).post('/branches').send(newBranch); + const branchId = createResponse.body.branchId; + + const updatedBranch = { + ...newBranch, + name: 'Updated Branch Name', + }; + const response = await request(app).put(`/branches/${branchId}`).send(updatedBranch); + expect(response.status).toBe(200); + expect(response.body.name).toBe('Updated Branch Name'); + }); + + it('should delete a branch by ID', async () => { + // First create a branch to test deleting it + const newBranch = { + headquartersId: 1, + name: 'Delete Me Branch', + description: 'This branch will be deleted', + address: '123 Delete St', + contactPerson: 'Delete Person', + email: 'delete@test.com', + phone: '555-9999', + }; + const createResponse = await request(app).post('/branches').send(newBranch); + const branchId = createResponse.body.branchId; + + const response = await request(app).delete(`/branches/${branchId}`); + expect(response.status).toBe(204); + }); + + it('should return 404 for non-existing branch', async () => { + const response = await request(app).get('/branches/999'); + expect(response.status).toBe(404); + }); +}); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.ts new file mode 100644 index 000000000..b1ffdccb8 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/branch.ts @@ -0,0 +1,176 @@ +/** + * @swagger + * tags: + * name: Branches + * description: API endpoints for managing branches + */ + +/** + * @swagger + * /api/branches: + * get: + * summary: Returns all branches + * tags: [Branches] + * responses: + * 200: + * description: List of all branches + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Branch' + * post: + * summary: Create a new branch + * tags: [Branches] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Branch' + * responses: + * 201: + * description: Branch created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Branch' + * + * /api/branches/{id}: + * get: + * summary: Get a branch by ID + * tags: [Branches] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Branch ID + * responses: + * 200: + * description: Branch found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Branch' + * 404: + * description: Branch not found + * put: + * summary: Update a branch + * tags: [Branches] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Branch ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Branch' + * responses: + * 200: + * description: Branch updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Branch' + * 404: + * description: Branch not found + * delete: + * summary: Delete a branch + * tags: [Branches] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Branch ID + * responses: + * 204: + * description: Branch deleted successfully + * 404: + * description: Branch not found + */ + +import express from 'express'; +import { Branch } from '../models/branch'; +import { getBranchesRepository } from '../repositories/branchesRepo'; +import { NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new branch +router.post('/', async (req, res, next) => { + try { + const repo = await getBranchesRepository(); + const newBranch = await repo.create(req.body as Omit); + res.status(201).json(newBranch); + } catch (error) { + next(error); + } +}); + +// Get all branches +router.get('/', async (req, res, next) => { + try { + const repo = await getBranchesRepository(); + const branches = await repo.findAll(); + res.json(branches); + } catch (error) { + next(error); + } +}); + +// Get a branch by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getBranchesRepository(); + const branch = await repo.findById(parseInt(req.params.id)); + if (branch) { + res.json(branch); + } else { + res.status(404).send('Branch not found'); + } + } catch (error) { + next(error); + } +}); + +// Update a branch by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getBranchesRepository(); + const updatedBranch = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedBranch); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Branch not found'); + } else { + next(error); + } + } +}); + +// Delete a branch by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getBranchesRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Branch not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/delivery.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/delivery.ts new file mode 100644 index 000000000..9bd7bf2df --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/delivery.ts @@ -0,0 +1,257 @@ +/** + * @swagger + * tags: + * name: Deliveries + * description: API endpoints for managing deliveries + */ + +/** + * @swagger + * /api/deliveries: + * get: + * summary: Returns all deliveries + * tags: [Deliveries] + * responses: + * 200: + * description: List of all deliveries + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Delivery' + * post: + * summary: Create a new delivery + * tags: [Deliveries] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Delivery' + * responses: + * 201: + * description: Delivery created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Delivery' + * + * /api/deliveries/{id}: + * get: + * summary: Get a delivery by ID + * tags: [Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Delivery ID + * responses: + * 200: + * description: Delivery found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Delivery' + * 404: + * description: Delivery not found + * put: + * summary: Update a delivery + * tags: [Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Delivery ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Delivery' + * responses: + * 200: + * description: Delivery updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Delivery' + * 404: + * description: Delivery not found + * delete: + * summary: Delete a delivery + * tags: [Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Delivery ID + * responses: + * 204: + * description: Delivery deleted successfully + * 404: + * description: Delivery not found + * + * /api/deliveries/{id}/status: + * put: + * summary: Update the status of a delivery + * tags: [Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Delivery ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - status + * properties: + * status: + * type: string + * description: The new status of the delivery + * deliveryPartner: + * type: string + * description: Optional delivery partner to notify via the notify-service about the status update + * responses: + * 200: + * description: Delivery status updated successfully + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/Delivery' + * - type: object + * properties: + * delivery: + * $ref: '#/components/schemas/Delivery' + * commandOutput: + * type: string + * description: Output from the notify-service command + * 404: + * description: Delivery not found + * 500: + * description: Error executing notify command + */ + +import express from 'express'; +import { Delivery } from '../models/delivery'; +import { exec } from 'child_process'; +import { getDeliveriesRepository } from '../repositories/deliveriesRepo'; +import { NotFoundError } from '../utils/errors'; + + +const router = express.Router(); + +// Create a new delivery +router.post('/', async (req, res, next) => { + try { + const repo = await getDeliveriesRepository(); + const newDelivery = await repo.create(req.body as Omit); + res.status(201).json(newDelivery); + } catch (error) { + next(error); + } +}); + +// Get all deliveries +router.get('/', async (req, res, next) => { + try { + const repo = await getDeliveriesRepository(); + const deliveries = await repo.findAll(); + res.json(deliveries); + } catch (error) { + next(error); + } +}); + +// Get a delivery by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getDeliveriesRepository(); + const delivery = await repo.findById(parseInt(req.params.id)); + if (delivery) { + res.json(delivery); + } else { + res.status(404).send('Delivery not found'); + } + } catch (error) { + next(error); + } +}); + +// Update the status of a delivery +router.put('/:id/status', async (req, res, next) => { + try { + const { status, deliveryPartner } = req.body; + const repo = await getDeliveriesRepository(); + const delivery = await repo.findById(parseInt(req.params.id)); + + if (delivery) { + const updatedDelivery = await repo.updateStatus(parseInt(req.params.id), status); + + if (deliveryPartner) { + exec(`notify ${deliveryPartner}`, (error, stdout) => { + if (error) { + console.error(`Error executing command: ${error}`); + return res.status(500).json({ error: error.message }); + } + res.json({ delivery: updatedDelivery, commandOutput: stdout }); + }); + } else { + res.json(updatedDelivery); + } + } else { + res.status(404).send('Delivery not found'); + } + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Delivery not found'); + } else { + next(error); + } + } +}); + +// Update a delivery by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getDeliveriesRepository(); + const updatedDelivery = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedDelivery); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Delivery not found'); + } else { + next(error); + } + } +}); + +// Delete a delivery by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getDeliveriesRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Delivery not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/headquarters.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/headquarters.ts new file mode 100644 index 000000000..b1cd4f1ce --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/headquarters.ts @@ -0,0 +1,335 @@ +/** + * @swagger + * tags: + * name: Headquarters + * description: API endpoints for managing headquarters locations + */ + +/** + * @swagger + * /api/headquarters: + * get: + * summary: Returns all headquarters + * tags: [Headquarters] + * responses: + * 200: + * description: List of all headquarters + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Headquarters' + * post: + * summary: Create a new headquarters + * tags: [Headquarters] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Headquarters' + * responses: + * 201: + * description: Headquarters created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Headquarters' + * + * /api/headquarters/{id}: + * get: + * summary: Get a headquarters by ID + * tags: [Headquarters] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Headquarters ID + * responses: + * 200: + * description: Headquarters found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Headquarters' + * 404: + * description: Headquarters not found + * put: + * summary: Update a headquarters + * tags: [Headquarters] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Headquarters ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Headquarters' + * responses: + * 200: + * description: Headquarters updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Headquarters' + * 404: + * description: Headquarters not found + * delete: + * summary: Delete a headquarters + * tags: [Headquarters] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Headquarters ID + * responses: + * 204: + * description: Headquarters deleted successfully + * 404: + * description: Headquarters not found + * + * /api/headquarters/{id}/metrics: + * get: + * summary: Get headquarters metrics by ID + * tags: [Headquarters] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Headquarters ID + * responses: + * 200: + * description: Headquarters metrics calculated + * content: + * application/json: + * schema: + * type: object + * properties: + * score: + * type: number + * description: Total score calculated from ID, floor count, and capacity + * average: + * type: number + * description: Average value + * display: + * type: string + * description: Display text + * 404: + * description: Headquarters not found + * + * /api/headquarters/{id}/label: + * get: + * summary: Get headquarters label by ID + * tags: [Headquarters] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Headquarters ID + * responses: + * 200: + * description: Headquarters label created + * content: + * application/json: + * schema: + * type: object + * properties: + * label: + * type: string + * description: Formatted location label + * 404: + * description: Headquarters not found + */ + +import express from 'express'; +import { Headquarters } from '../models/headquarters'; +import { getHeadquartersRepository } from '../repositories/headquartersRepo'; +import { NotFoundError } from '../utils/errors'; + +const router = express.Router(); + + +// Get all headquarters +router.get('/', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + const headquarters = await repo.findAll(); + res.json(headquarters); + } catch (error) { + next(error); + } +}); + +// Get a headquarters by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + const headquarters = await repo.findById(parseInt(req.params.id)); + if (headquarters) { + res.json(headquarters); + } else { + res.status(404).send('Headquarters not found'); + } + } catch (error) { + next(error); + } +}); + +// Create a new headquarters +router.post('/', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + + const hqValidator = new (HeadquartersValidator as any)(req.body.name, req.body.address); + if (!hqValidator.isValid()) { + res.status(400).send('Invalid headquarters data'); + return; + } + + const newHeadquarters = await repo.create(req.body as Omit); + res.status(201).json(newHeadquarters); + } catch (error) { + next(error); + } +}); + +// Update a headquarters by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + + const hqValidator = (HeadquartersValidator as any)(req.body.name, req.body.address); + if (!hqValidator.isValid()) { + res.status(400).send('Invalid headquarters data'); + return; + } + + const updatedHeadquarters = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedHeadquarters); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Headquarters not found'); + } else { + next(error); + } + } +}); + + +// Delete a headquarters by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Headquarters not found'); + } else { + next(error); + } + } +}); + +// Get headquarters metrics by ID +router.get('/:id/metrics', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + const headquarters = await repo.findById(parseInt(req.params.id)); + if (!headquarters) { + res.status(404).send('Headquarters not found'); + return; + } + + const metrics = calculateHeadquartersMetrics( + headquarters.headquartersId, + headquarters.floorCount || 0, + headquarters.capacity || 0 + ); + res.json(metrics); + } catch (error) { + next(error); + } +}); + +// Get headquarters label by ID +router.get('/:id/label', async (req, res, next) => { + try { + const repo = await getHeadquartersRepository(); + const headquarters = await repo.findById(parseInt(req.params.id)); + if (!headquarters) { + res.status(404).send('Headquarters not found'); + return; + } + + const label = createLocationLabel( + headquarters.name, + headquarters.city || '', + headquarters.country || '' + ); + res.json({ label }); + } catch (error) { + next(error); + } +}); + + + +// Inconsistent use of new: helper function used both as constructor and regular function +function HeadquartersValidator(this: any, name: any, address: any) { + if(!validateHQName(name)) { + throw new Error('Invalid headquarters name'); + }; + + this.name = name; + this.address = address; + this.isValid = function () { + return this.name && this.address; + }; +} + +// Missing space in concatenation example +function createLocationLabel(name: string, city: string, country: string): string { + const label = `Location:${ name }City:${ city }Country:${ country}`; // Missing spaces + return label; +} + +// Implicit operand conversion example +function calculateHeadquartersMetrics(id: any, floorCount: any, capacity: any): any { + // This will cause implicit conversion issues when mixed types are passed + const totalScore = id + floorCount + capacity; // Could be string concatenation or numeric addition + const averageValue = (id + floorCount) / 2; // Mixed type division + const displayText = `HQ-${ id }${floorCount}`; // Implicit string conversion + + return { + score: totalScore, + average: averageValue, + display: displayText + }; +} + +// Misleading indentation example +function validateHQName(hq: any): boolean { + if (hq.name) + console.log('Name is valid'); + return true; // This appears to be part of the if, but it's not! + console.log('Name is invalid'); + + return false; +} + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/order.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/order.ts new file mode 100644 index 000000000..b78ec3779 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/order.ts @@ -0,0 +1,183 @@ +/** + * @swagger + * tags: + * name: Orders + * description: API endpoints for managing orders + */ + +/** + * @swagger + * /api/orders: + * get: + * summary: Returns all orders + * tags: [Orders] + * responses: + * 200: + * description: List of all orders + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Order' + * post: + * summary: Create a new order + * tags: [Orders] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Order' + * responses: + * 201: + * description: Order created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Order' + * + * /api/orders/{id}: + * get: + * summary: Get an order by ID + * tags: [Orders] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * responses: + * 200: + * description: Order found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Order' + * 404: + * description: Order not found + * put: + * summary: Update an order + * tags: [Orders] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Order' + * responses: + * 200: + * description: Order updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Order' + * 404: + * description: Order not found + * delete: + * summary: Delete an order + * tags: [Orders] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * responses: + * 204: + * description: Order deleted successfully + * 404: + * description: Order not found + */ + +import express from 'express'; +import { Order } from '../models/order'; +import { getOrdersRepository } from '../repositories/ordersRepo'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new order +router.post('/', async (req, res, next) => { + try { + const repo = await getOrdersRepository(); + const newOrder = await repo.create(req.body as Omit); + res.status(201).json(newOrder); + } catch (error) { + next(error); + } +}); + +// Get all orders +router.get('/', async (req, res, next) => { + try { + const repo = await getOrdersRepository(); + const orders = await repo.findAll(); + + // Non-linear pattern example: duplicate destructuring in object + if (orders.length > 0) { + const { orderId: id, orderId: duplicateId } = orders[0]; + console.log('Non-linear pattern in order routes:', id, duplicateId); + } + + res.json(orders); + } catch (error) { + next(error); + } +}); + +// Get an order by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getOrdersRepository(); + const order = await repo.findById(parseInt(req.params.id)); + if (order) { + res.json(order); + } else { + res.status(404).send('Order not found'); + } + } catch (error) { + next(error); + } +}); + +// Update an order by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getOrdersRepository(); + const updatedOrder = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedOrder); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order not found'); + } else { + next(error); + } + } +}); + +// Delete an order by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getOrdersRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetail.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetail.ts new file mode 100644 index 000000000..724edccf1 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetail.ts @@ -0,0 +1,176 @@ +/** + * @swagger + * tags: + * name: Order Details + * description: API endpoints for managing order details + */ + +/** + * @swagger + * /api/order-details: + * get: + * summary: Returns all order details + * tags: [Order Details] + * responses: + * 200: + * description: List of all order details + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/OrderDetail' + * post: + * summary: Create a new order detail + * tags: [Order Details] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetail' + * responses: + * 201: + * description: Order detail created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetail' + * + * /api/order-details/{id}: + * get: + * summary: Get an order detail by ID + * tags: [Order Details] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail ID + * responses: + * 200: + * description: Order detail found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetail' + * 404: + * description: Order detail not found + * put: + * summary: Update an order detail + * tags: [Order Details] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetail' + * responses: + * 200: + * description: Order detail updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetail' + * 404: + * description: Order detail not found + * delete: + * summary: Delete an order detail + * tags: [Order Details] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail ID + * responses: + * 204: + * description: Order detail deleted successfully + * 404: + * description: Order detail not found + */ + +import express from 'express'; +import { OrderDetail } from '../models/orderDetail'; +import { getOrderDetailsRepository } from '../repositories/orderDetailsRepo'; +import { NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new order detail +router.post('/', async (req, res, next) => { + try { + const repo = await getOrderDetailsRepository(); + const newOrderDetail = await repo.create(req.body as Omit); + res.status(201).json(newOrderDetail); + } catch (error) { + next(error); + } +}); + +// Get all order details +router.get('/', async (req, res, next) => { + try { + const repo = await getOrderDetailsRepository(); + const orderDetails = await repo.findAll(); + res.json(orderDetails); + } catch (error) { + next(error); + } +}); + +// Get an order detail by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailsRepository(); + const orderDetail = await repo.findById(parseInt(req.params.id)); + if (orderDetail) { + res.json(orderDetail); + } else { + res.status(404).send('Order detail not found'); + } + } catch (error) { + next(error); + } +}); + +// Update an order detail by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailsRepository(); + const updatedOrderDetail = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedOrderDetail); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order detail not found'); + } else { + next(error); + } + } +}); + +// Delete an order detail by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailsRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order detail not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetailDelivery.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetailDelivery.ts new file mode 100644 index 000000000..8f910da43 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/orderDetailDelivery.ts @@ -0,0 +1,178 @@ +/** + * @swagger + * tags: + * name: Order Detail Deliveries + * description: API endpoints for managing order detail deliveries + */ + +/** + * @swagger + * /api/order-detail-deliveries: + * get: + * summary: Returns all order detail deliveries + * tags: [Order Detail Deliveries] + * responses: + * 200: + * description: List of all order detail deliveries + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/OrderDetailDelivery' + * post: + * summary: Create a new order detail delivery + * tags: [Order Detail Deliveries] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetailDelivery' + * responses: + * 201: + * description: Order detail delivery created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetailDelivery' + * + * /api/order-detail-deliveries/{id}: + * get: + * summary: Get an order detail delivery by ID + * tags: [Order Detail Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail delivery ID + * responses: + * 200: + * description: Order detail delivery found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetailDelivery' + * 404: + * description: Order detail delivery not found + * put: + * summary: Update an order detail delivery + * tags: [Order Detail Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail delivery ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetailDelivery' + * responses: + * 200: + * description: Order detail delivery updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrderDetailDelivery' + * 404: + * description: Order detail delivery not found + * delete: + * summary: Delete an order detail delivery + * tags: [Order Detail Deliveries] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order detail delivery ID + * responses: + * 204: + * description: Order detail delivery deleted successfully + * 404: + * description: Order detail delivery not found + */ + +import express from 'express'; +import { OrderDetailDelivery } from '../models/orderDetailDelivery'; +import { getOrderDetailDeliveriesRepository } from '../repositories/orderDetailDeliveriesRepo'; +import { NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new order detail delivery +router.post('/', async (req, res, next) => { + try { + const repo = await getOrderDetailDeliveriesRepository(); + const newOrderDetailDelivery = await repo.create( + req.body as Omit, + ); + res.status(201).json(newOrderDetailDelivery); + } catch (error) { + next(error); + } +}); + +// Get all order detail deliveries +router.get('/', async (req, res, next) => { + try { + const repo = await getOrderDetailDeliveriesRepository(); + const orderDetailDeliveries = await repo.findAll(); + res.json(orderDetailDeliveries); + } catch (error) { + next(error); + } +}); + +// Get an order detail delivery by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailDeliveriesRepository(); + const orderDetailDelivery = await repo.findById(parseInt(req.params.id)); + if (orderDetailDelivery) { + res.json(orderDetailDelivery); + } else { + res.status(404).send('Order detail delivery not found'); + } + } catch (error) { + next(error); + } +}); + +// Update an order detail delivery by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailDeliveriesRepository(); + const updatedOrderDetailDelivery = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedOrderDetailDelivery); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order detail delivery not found'); + } else { + next(error); + } + } +}); + +// Delete an order detail delivery by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getOrderDetailDeliveriesRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Order detail delivery not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/product.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/product.ts new file mode 100644 index 000000000..47a9c6557 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/product.ts @@ -0,0 +1,176 @@ +/** + * @swagger + * tags: + * name: Products + * description: API endpoints for managing products + */ + +/** + * @swagger + * /api/products: + * get: + * summary: Returns all products + * tags: [Products] + * responses: + * 200: + * description: List of all products + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Product' + * post: + * summary: Create a new product + * tags: [Products] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * responses: + * 201: + * description: Product created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * + * /api/products/{id}: + * get: + * summary: Get a product by ID + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Product ID + * responses: + * 200: + * description: Product found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * 404: + * description: Product not found + * put: + * summary: Update a product + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Product ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * responses: + * 200: + * description: Product updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * 404: + * description: Product not found + * delete: + * summary: Delete a product + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Product ID + * responses: + * 204: + * description: Product deleted successfully + * 404: + * description: Product not found + */ + +import express from 'express'; +import { Product } from '../models/product'; +import { getProductsRepository } from '../repositories/productsRepo'; +import { NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new product +router.post('/', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const newProduct = await repo.create(req.body as Omit); + res.status(201).json(newProduct); + } catch (error) { + next(error); + } +}); + +// Get all products +router.get('/', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const products = await repo.findAll(); + res.json(products); + } catch (error) { + next(error); + } +}); + +// Get a product by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const product = await repo.findById(parseInt(req.params.id)); + if (product) { + res.json(product); + } else { + res.status(404).send('Product not found'); + } + } catch (error) { + next(error); + } +}); + +// Update a product by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const updatedProduct = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedProduct); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Product not found'); + } else { + next(error); + } + } +}); + +// Delete a product by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Product not found'); + } else { + next(error); + } + } +}); + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/supplier.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/supplier.ts new file mode 100644 index 000000000..f2401bb3c --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/routes/supplier.ts @@ -0,0 +1,234 @@ +/** + * @swagger + * tags: + * name: Suppliers + * description: API endpoints for managing suppliers + */ + +/** + * @swagger + * /api/suppliers: + * get: + * summary: Returns all suppliers + * tags: [Suppliers] + * responses: + * 200: + * description: List of all suppliers + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Supplier' + * post: + * summary: Create a new supplier + * tags: [Suppliers] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Supplier' + * responses: + * 201: + * description: Supplier created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Supplier' + * + * /api/suppliers/{id}: + * get: + * summary: Get a supplier by ID + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Supplier ID + * responses: + * 200: + * description: Supplier found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Supplier' + * 404: + * description: Supplier not found + * put: + * summary: Update a supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Supplier ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Supplier' + * responses: + * 200: + * description: Supplier updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Supplier' + * 404: + * description: Supplier not found + * delete: + * summary: Delete a supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Supplier ID + * responses: + * 204: + * description: Supplier deleted successfully + * 404: + * description: Supplier not found + * + * /api/suppliers/{id}/status: + * get: + * summary: Get the status of a supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Supplier ID + * responses: + * 200: + * description: Supplier status + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * enum: [INACTIVE, APPROVED, PENDING] + * 404: + * description: Supplier not found + */ + +import express from 'express'; +import { Supplier } from '../models/supplier'; +import { getSuppliersRepository } from '../repositories/suppliersRepo'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; + +const router = express.Router(); + +// Create a new supplier +router.post('/', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + const newSupplier = await repo.create(req.body as Omit); + res.status(201).json(newSupplier); + } catch (error) { + next(error); + } +}); + +// Get all suppliers +router.get('/', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + const suppliers = await repo.findAll(); + res.json(suppliers); + } catch (error) { + next(error); + } +}); + +// Get a supplier by ID +router.get('/:id', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + const supplier = await repo.findById(parseInt(req.params.id)); + if (supplier) { + res.json(supplier); + } else { + res.status(404).send('Supplier not found'); + } + } catch (error) { + next(error); + } +}); + + +// Update a supplier by ID +router.put('/:id', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + const updatedSupplier = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedSupplier); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Supplier not found'); + } else { + next(error); + } + } +}); + +// Delete a supplier by ID +router.delete('/:id', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send('Supplier not found'); + } else { + next(error); + } + } +}); + +// Get supplier status by ID +router.get('/:id/status', async (req, res, next) => { + try { + const repo = await getSuppliersRepository(); + const supplier = await repo.findById(parseInt(req.params.id)); + if (!supplier) { + res.status(404).send('Supplier not found'); + return; + } + + const status = processSupplierStatus(supplier); + + res.json({ status }); + } catch (error) { + next(error); + } +}); + +// Misleading indentation example +function processSupplierStatus(supplier: Supplier): string { + if (supplier.active) + console.log('Supplier is active'); + return 'APPROVED'; + + if (supplier.verified) + console.log('Supplier verified'); + console.log('Setting up account'); // This also appears conditional but always executes + + return 'PENDING'; + +} + +export default router; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/seedData.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/seedData.ts new file mode 100644 index 000000000..3a7ceb2b2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/seedData.ts @@ -0,0 +1,312 @@ +import { Supplier } from './models/supplier'; +import { Product } from './models/product'; +import { Headquarters } from './models/headquarters'; +import { Branch } from './models/branch'; +import { Order } from './models/order'; +import { OrderDetail } from './models/orderDetail'; +import { Delivery } from './models/delivery'; +import { OrderDetailDelivery } from './models/orderDetailDelivery'; + +// Suppliers +export const suppliers: Supplier[] = [ + { + supplierId: 1, + name: 'PurrTech Innovations', + description: 'Leading supplier of premium smart cat technology', + contactPerson: 'Felix Whiskerton', + email: 'felix@purrtech.co', + phone: '555-0101', + active: true, + verified: true, + }, + { + supplierId: 2, + name: 'WhiskerWare Systems', + description: 'Advanced feline-focused smart product supplier', + contactPerson: 'Tabitha Pawson', + email: 'tabitha@whiskerware.com', + phone: '555-0102', + active: true, + verified: false, + }, + { + supplierId: 3, + name: 'CatNip Creations', + description: 'Supplier of eco-friendly cat toys and accessories', + contactPerson: 'Nina Nibbles', + email: 'nina@catnip.com', + phone: '555-0103', + active: false, + verified: false + }, +]; + +// Products +export const products: Product[] = [ + { + productId: 1, + supplierId: 3, + name: 'SmartFeeder One', + description: + "This AI-powered feeder learns your cat's snack schedule based on nap cycles and mealtime habits. It detects overeating, undernapping, and auto-updates a Feline Health Repo.", + price: 129.99, + sku: 'CAT-FEED-001', + unit: 'piece', + imgName: 'feeder.png', + discount: 0.25, + }, + { + productId: 2, + supplierId: 3, + name: 'AutoClean Litter Dome', + description: + "A self-cleaning litter box that detects patterns in your cat's... commits. Sends you a health report and Slack alert if things look off.", + price: 199.99, + sku: 'CAT-LITTER-001', + unit: 'piece', + imgName: 'litter-box.png', + discount: 0.25, + }, + { + productId: 3, + supplierId: 2, + name: 'CatFlix Entertainment Portal', + description: + 'On-demand laser shows, motion videos, and bird-watching streams - customized per cat using AI interest tracking. Think Netflix, but for felines.', + price: 89.99, + sku: 'CAT-FLIX-001', + unit: 'piece', + imgName: 'catflix.png', + }, + { + productId: 4, + supplierId: 2, + name: 'PawTrack Smart Collar', + description: + 'GPS and activity tracker with AI-powered mood detection based on tail position, purring frequency, and movement patterns. Syncs with your phone for walk stats and zoomie alerts.', + price: 79.99, + sku: 'CAT-COLLAR-001', + unit: 'piece', + imgName: 'smart-collar.png', + }, + { + productId: 5, + supplierId: 1, + name: 'SleepNest ThermoPod', + description: + "A smart bed that adjusts its temperature, lighting, and white noise based on your cat's REM cycles. Auto-generates nap metrics in JSON.", + price: 149.99, + sku: 'CAT-BED-001', + unit: 'piece', + imgName: 'sleep-nest.png', + }, + { + productId: 6, + supplierId: 1, + name: 'ClawMate Auto Groomer', + description: + 'Your cat brushes itself. This AI station detects which areas need grooming, dispenses treats for patience, and logs grooming history to your pet portal.', + price: 119.99, + sku: 'CAT-GROOM-001', + unit: 'piece', + imgName: 'auto-groomer.png', + }, + { + productId: 7, + supplierId: 3, + name: 'Smart Fountain Flow+', + description: + 'This water fountain adjusts flow patterns based on time of day, cat hydration levels, and even playfulness. Uses facial recognition to distinguish multiple cats.', + price: 69.99, + sku: 'CAT-FOUNTAIN-001', + unit: 'piece', + imgName: 'smart-fountain.png', + discount: 0.25, + }, + { + productId: 8, + supplierId: 2, + name: 'ScratchPad Pro', + description: + 'More than a scratcher - this one detects scratching habits, gamifies it with leaderboard stats for multi-cat homes, and awards digital badges.', + price: 59.99, + sku: 'CAT-SCRATCH-001', + unit: 'piece', + imgName: 'scratch-pad.png', + }, + { + productId: 9, + supplierId: 2, + name: 'ChirpCam Window Mount', + description: + "Motion-activated smart cam that records wildlife outside the window and sends curated 'Birdflix' highlights to your cat's personal feed.", + price: 99.99, + sku: 'CAT-CAM-001', + unit: 'piece', + imgName: 'chirp-cam.png', + }, + { + productId: 10, + supplierId: 3, + name: 'SnackVault Puzzle Dispenser', + description: + "Treat puzzle toy that evolves in difficulty with your cat's cleverness. AI engine auto-adjusts pathways and provides tips to the human if the cat cheats.", + price: 49.99, + sku: 'CAT-SNACK-001', + unit: 'piece', + imgName: 'snack-vault.png', + discount: 0.25, + }, + { + productId: 11, + supplierId: 1, + name: 'DoorDash Pet Portal', + description: + 'Smart cat door with facial recognition and time-based access. Prevents midnight squirrel parties and tracks in/out commits to your dashboard.', + price: 159.99, + sku: 'CAT-DOOR-001', + unit: 'piece', + imgName: 'door-dash.png', + }, + { + productId: 12, + supplierId: 2, + name: 'ZoomieTracker AI Mat', + description: + 'A motion-sensing mat that detects zoomies, spins up chase lights, and logs agility bursts to a weekly health report. Yes, it graphs zoomies per hour.', + price: 79.99, + sku: 'CAT-TRACKER-001', + unit: 'piece', + imgName: 'tracker-mat.png', + }, +]; + +// Headquarters +export const headquarters: Headquarters[] = [ + { + headquartersId: 1, + name: 'CatTech Global HQ', + description: 'Feline tech innovations headquarters', + address: '123 Whisker Lane, Purrington District', + contactPerson: 'Catherine Purrston', + email: 'catherine@octocat.com', + phone: '555-0001', + }, +]; + +// Branches +export const branches: Branch[] = [ + { + branchId: 1, + headquartersId: 1, + name: 'Meowtown Branch', + description: 'Main downtown cat tech showroom', + address: '456 Purrfect Plaza', + contactPerson: 'Chloe Whiskers', + email: 'cwhiskers@octocat.com', + phone: '555-0201', + }, + { + branchId: 2, + headquartersId: 1, + name: 'Tabby Terrace Branch', + description: 'Western district cat tech hub', + address: '789 Feline Avenue', + contactPerson: 'Tom Pouncer', + email: 'tpouncer@octocat.com', + phone: '555-0202', + }, +]; + +// Orders +export const orders: Order[] = [ + { + orderId: 1, + branchId: 1, + orderDate: new Date().toISOString(), + name: 'Q2 Feline Tech Refresh', + description: 'Quarterly smart cat tech product refresh', + status: 'pending', + }, + { + orderId: 2, + branchId: 2, + orderDate: new Date().toISOString(), + name: 'Cat Enrichment Bundle', + description: 'Monthly cat entertainment systems restock', + status: 'processing', + }, +]; + +// Order Details +export const orderDetails: OrderDetail[] = [ + { + orderDetailId: 1, + orderId: 1, + productId: 2, + quantity: 5, + unitPrice: 199.99, + notes: 'AutoClean Litter Domes for new cat café locations', + }, + { + orderDetailId: 2, + orderId: 1, + productId: 3, + quantity: 5, + unitPrice: 89.99, + notes: 'CatFlix Entertainment Portals for waiting areas', + }, + { + orderDetailId: 3, + orderId: 2, + productId: 4, + quantity: 20, + unitPrice: 79.99, + notes: 'PawTrack Smart Collars for adoption events', + }, +]; + +// Deliveries +export const deliveries: Delivery[] = [ + { + deliveryId: 1, + supplierId: 1, + deliveryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now + name: 'PurrTech Smart Home Bundle', + description: 'Premium cat tech products delivery for smart cat homes', + status: 'pending', + }, + { + deliveryId: 2, + supplierId: 2, + deliveryDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days from now + name: 'WhiskerWare Entertainment Package', + description: 'Entertainment and tracking systems for feline companions', + status: 'in-transit', + }, +]; + +// Order Detail Deliveries +export const orderDetailDeliveries: OrderDetailDelivery[] = [ + { + orderDetailDeliveryId: 1, + orderDetailId: 1, + deliveryId: 1, + quantity: 5, + notes: 'Delivery batch', + }, + { + orderDetailDeliveryId: 2, + orderDetailId: 2, + deliveryId: 1, + quantity: 5, + notes: 'Delivery batch', + }, + { + orderDetailDeliveryId: 3, + orderDetailId: 3, + deliveryId: 2, + quantity: 20, + notes: 'Delivery', + }, +]; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/errors.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/errors.ts new file mode 100644 index 000000000..a0dcd960e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/errors.ts @@ -0,0 +1,94 @@ +import type { Request, Response, NextFunction } from 'express'; +/** + * Error handling utilities for database operations + */ + +export class DatabaseError extends Error { + public readonly code: string; + public readonly statusCode: number; + + constructor(message: string, code: string = 'DATABASE_ERROR', statusCode: number = 500) { + super(message); + this.name = 'DatabaseError'; + this.code = code; + this.statusCode = statusCode; + } +} + +export class NotFoundError extends DatabaseError { + constructor(entity: string, id: string | number) { + super(`${entity} with ID ${id} not found`, 'NOT_FOUND', 404); + this.name = 'NotFoundError'; + } +} + +export class ValidationError extends DatabaseError { + constructor(message: string) { + super(`Validation error: ${message}`, 'VALIDATION_ERROR', 400); + this.name = 'ValidationError'; + } +} + +export class ConflictError extends DatabaseError { + constructor(message: string) { + super(`Conflict: ${message}`, 'CONFLICT', 409); + this.name = 'ConflictError'; + } +} + +/** + * Handle database errors and convert SQLite-specific errors to appropriate types + */ +export function handleDatabaseError(error: unknown, entity?: string, id?: string | number): never { + if(!(error instanceof DatabaseError)) { + const message = error instanceof Error ? error.message : error; + + // Default to generic database error + throw new DatabaseError(`Database operation failed: ${message}`, 'DATABASE_ERROR', 500); + } + // SQLite constraint violation (UNIQUE, FOREIGN KEY, etc.) + if (error.code === 'SQLITE_CONSTRAINT') { + if (error.message.includes('UNIQUE')) { + throw new ConflictError('Resource already exists'); + } + if (error.message.includes('FOREIGN KEY')) { + throw new ValidationError('Invalid reference to related entity'); + } + throw new ValidationError(error.message); + } + + // SQLite busy/locked database + if (error.code === 'SQLITE_BUSY') { + throw new DatabaseError('Database is temporarily unavailable', 'DATABASE_BUSY', 503); + } + + // Handle case where no rows were affected (for updates/deletes) + if (error.message && error.message.includes('No rows affected') && entity && id) { + throw new NotFoundError(entity, id); + } + + throw error; +} + +/** + * Express error handler middleware for database errors + */ +export function errorHandler(error: unknown, _req: Request, res: Response, _next: NextFunction): void { + if (error instanceof DatabaseError) { + res.status(error.statusCode).json({ + error: { + code: error.code, + message: error.message, + }, + }); + return; + } + + // Default error handling + res.status(500).json({ + error: { + code: 'INTERNAL_ERROR', + message: 'An unexpected error occurred', + }, + }); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/sql.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/sql.ts new file mode 100644 index 000000000..2b1a76c59 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/src/utils/sql.ts @@ -0,0 +1,214 @@ +/** + * SQL helper utilities + */ + +export interface QueryBuilder { + select: string[]; + from: string; + joins: string[]; + where: string[]; + orderBy: string[]; + limit?: number; + offset?: number; +} + +/** + * Simple query builder for SELECT statements + */ +export class SelectQueryBuilder { + private query: QueryBuilder; + + constructor(table: string) { + this.query = { + select: ['*'], + from: table, + joins: [], + where: [], + orderBy: [], + }; + } + + /** + * Specify columns to select + */ + public select(columns: string[]): this { + this.query.select = columns; + return this; + } + + /** + * Add JOIN clause + */ + public join(table: string, condition: string, type: 'INNER' | 'LEFT' | 'RIGHT' = 'INNER'): this { + this.query.joins.push(`${type} JOIN ${table} ON ${condition}`); + return this; + } + + /** + * Add WHERE condition + */ + public where(condition: string): this { + this.query.where.push(condition); + return this; + } + + /** + * Add ORDER BY clause + */ + public orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this { + this.query.orderBy.push(`${column} ${direction}`); + return this; + } + + /** + * Set LIMIT + */ + public limit(count: number): this { + this.query.limit = count; + return this; + } + + /** + * Set OFFSET + */ + public offset(count: number): this { + this.query.offset = count; + return this; + } + + /** + * Build the final SQL query + */ + public build(): string { + let sql = `SELECT ${this.query.select.join(', ')} FROM ${this.query.from}`; + + if (this.query.joins.length > 0) { + sql += ` ${ this.query.joins.join(' ')}`; + } + + if (this.query.where.length > 0) { + sql += ` WHERE ${ this.query.where.join(' AND ')}`; + } + + if (this.query.orderBy.length > 0) { + sql += ` ORDER BY ${ this.query.orderBy.join(', ')}`; + } + + if (this.query.limit !== undefined) { + sql += ` LIMIT ${this.query.limit}`; + } + + if (this.query.offset !== undefined) { + sql += ` OFFSET ${this.query.offset}`; + } + + return sql; + } +} + +/** + * Convert camelCase to snake_case for database columns + */ +export function toSnakeCase(str: string): string { + return str.replace(/([A-Z])/g, '_$1').toLowerCase(); +} + +/** + * Convert snake_case to camelCase for JavaScript objects + */ +export function toCamelCase(str: string): string { + return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +/** + * Convert object keys from camelCase to snake_case + */ +export function objectToSnakeCase>(obj: T): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + result[toSnakeCase(key)] = value; + } + + return result; +} + +/** + * Type for database row that can be converted to a model + */ +export type DatabaseRow = Record; + +/** + * Convert object keys from snake_case to camelCase with typed output + */ +export function objectToCamelCase(obj: DatabaseRow): T { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + result[toCamelCase(key)] = value; + } + + return result as T; +} + +/** + * Convert multiple database rows to typed models + */ +export function mapDatabaseRows(rows: DatabaseRow[]): T[] { + return rows.map((row) => objectToCamelCase(row)); +} + +/** + * Generate placeholders for parameterized queries + * @param count Number of placeholders needed + * @returns String of comma-separated placeholders like "?, ?, ?" + */ +export function generatePlaceholders(count: number): string { + return Array(count).fill('?').join(', '); +} + +/** + * Build INSERT SQL with placeholders + */ +export function buildInsertSQL>( + table: string, + data: T, +): { sql: string; values: unknown[] } { + const snakeCaseData = objectToSnakeCase(data); + const columns = Object.keys(snakeCaseData); + const values = Object.values(snakeCaseData); + const placeholders = generatePlaceholders(columns.length); + + const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`; + + return { sql, values }; +} + +/** + * Build UPDATE SQL with placeholders + */ +export function buildUpdateSQL>( + table: string, + data: Partial, + whereClause: string, +): { sql: string; values: unknown[] } { + const snakeCaseData = objectToSnakeCase(data as Record); + const columns = Object.keys(snakeCaseData); + const values = Object.values(snakeCaseData); + + const setClause = columns.map((col) => `${col} = ?`).join(', '); + const sql = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`; + + return { sql, values }; +} + +/** + * Validate required fields in an object + */ +export function validateRequiredFields>(obj: T, requiredFields: string[]): void { + for (const field of requiredFields) { + if (obj[field] === undefined || obj[field] === null || obj[field] === '') { + throw new Error(`Required field '${field}' is missing or empty`); + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/tsconfig.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/tsconfig.json new file mode 100644 index 000000000..858db8413 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/vitest.config.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/vitest.config.ts new file mode 100644 index 000000000..d368711ef --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/api-ts/vitest.config.ts @@ -0,0 +1,14 @@ +/// +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + coverage: { + // Include json-summary so CI can read api/coverage/coverage-summary.json + reporter: ['text', 'json', 'json-summary', 'html'], + }, + exclude: ['dist/**', 'node_modules/**', 'database/**'] + }, +}); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/001_init.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/001_init.sql new file mode 100644 index 000000000..267a0e8c7 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/001_init.sql @@ -0,0 +1,108 @@ +-- Initial database schema for OctoCAT Supply Chain Management +-- Migration 001: Create core tables + +-- Create suppliers table +CREATE TABLE suppliers ( + supplier_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + contact_person TEXT, + email TEXT, + phone TEXT +); + +-- Create headquarters table +CREATE TABLE headquarters ( + headquarters_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + address TEXT, + contact_person TEXT, + email TEXT, + phone TEXT +); + +-- Create branches table (references headquarters) +CREATE TABLE branches ( + branch_id INTEGER PRIMARY KEY, + headquarters_id INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + address TEXT, + contact_person TEXT, + email TEXT, + phone TEXT, + FOREIGN KEY (headquarters_id) REFERENCES headquarters(headquarters_id) ON DELETE CASCADE +); + +-- Create products table (references suppliers) +CREATE TABLE products ( + product_id INTEGER PRIMARY KEY, + supplier_id INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + price REAL NOT NULL, + sku TEXT NOT NULL, + unit TEXT NOT NULL, + img_name TEXT, + discount REAL DEFAULT 0.0, + FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) ON DELETE CASCADE +); + +-- Create orders table (references branches) +CREATE TABLE orders ( + order_id INTEGER PRIMARY KEY, + branch_id INTEGER NOT NULL, + order_date TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'pending', + FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE CASCADE +); + +-- Create order_details table (references orders and products) +CREATE TABLE order_details ( + order_detail_id INTEGER PRIMARY KEY, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + unit_price REAL NOT NULL, + notes TEXT, + FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE +); + +-- Create deliveries table (references suppliers) +CREATE TABLE deliveries ( + delivery_id INTEGER PRIMARY KEY, + supplier_id INTEGER NOT NULL, + delivery_date TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'pending', + FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) ON DELETE CASCADE +); + +-- Create order_detail_deliveries table (junction table) +CREATE TABLE order_detail_deliveries ( + order_detail_delivery_id INTEGER PRIMARY KEY, + order_detail_id INTEGER NOT NULL, + delivery_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + notes TEXT, + FOREIGN KEY (order_detail_id) REFERENCES order_details(order_detail_id) ON DELETE CASCADE, + FOREIGN KEY (delivery_id) REFERENCES deliveries(delivery_id) ON DELETE CASCADE +); + +-- Create indexes for better performance +CREATE INDEX idx_branches_headquarters_id ON branches(headquarters_id); +CREATE INDEX idx_products_supplier_id ON products(supplier_id); +CREATE INDEX idx_products_sku ON products(sku); +CREATE INDEX idx_orders_branch_id ON orders(branch_id); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_order_details_order_id ON order_details(order_id); +CREATE INDEX idx_order_details_product_id ON order_details(product_id); +CREATE INDEX idx_deliveries_supplier_id ON deliveries(supplier_id); +CREATE INDEX idx_deliveries_status ON deliveries(status); +CREATE INDEX idx_order_detail_deliveries_order_detail_id ON order_detail_deliveries(order_detail_id); +CREATE INDEX idx_order_detail_deliveries_delivery_id ON order_detail_deliveries(delivery_id); \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/002_add_supplier_status_fields.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/002_add_supplier_status_fields.sql new file mode 100644 index 000000000..bf3646e4b --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/migrations/002_add_supplier_status_fields.sql @@ -0,0 +1,4 @@ +-- Migration 002: Add active and verified fields to suppliers table + +ALTER TABLE suppliers ADD COLUMN active INTEGER NOT NULL DEFAULT 1; +ALTER TABLE suppliers ADD COLUMN verified INTEGER NOT NULL DEFAULT 0; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/001_suppliers.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/001_suppliers.sql new file mode 100644 index 000000000..1baaf5d7d --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/001_suppliers.sql @@ -0,0 +1,5 @@ +-- Seed data for suppliers +INSERT INTO suppliers (supplier_id, name, description, contact_person, email, phone, active, verified) VALUES +(1, 'PurrTech Innovations', 'Leading supplier of premium smart cat technology', 'Felix Whiskerton', 'felix@purrtech.co', '555-0101', 1, 1), +(2, 'WhiskerWare Systems', 'Advanced feline-focused smart product supplier', 'Tabitha Pawson', 'tabitha@whiskerware.com', '555-0102', 1, 0), +(3, 'CatNip Creations', 'Supplier of eco-friendly cat toys and accessories', 'Nina Nibbles', 'nina@catnip.com', '555-0103', 0, 0); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/002_headquarters.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/002_headquarters.sql new file mode 100644 index 000000000..222d567c2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/002_headquarters.sql @@ -0,0 +1,3 @@ +-- Seed data for headquarters +INSERT INTO headquarters (headquarters_id, name, description, address, contact_person, email, phone) VALUES +(1, 'CatTech Global HQ', 'Feline tech innovations headquarters', '123 Whisker Lane, Purrington District', 'Catherine Purrston', 'catherine@octocat.com', '555-0001'); \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/003_branches.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/003_branches.sql new file mode 100644 index 000000000..c75c64942 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/003_branches.sql @@ -0,0 +1,4 @@ +-- Seed data for branches +INSERT INTO branches (branch_id, headquarters_id, name, description, address, contact_person, email, phone) VALUES +(1, 1, 'Meowtown Branch', 'Main downtown cat tech showroom', '456 Purrfect Plaza', 'Chloe Whiskers', 'cwhiskers@octocat.com', '555-0201'), +(2, 1, 'Tabby Terrace Branch', 'Western district cat tech hub', '789 Feline Avenue', 'Tom Pouncer', 'tpouncer@octocat.com', '555-0202'); \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/004_products.sql b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/004_products.sql new file mode 100644 index 000000000..ba1a3d054 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/database/seed/004_products.sql @@ -0,0 +1,14 @@ +-- Seed data for products +INSERT INTO products (product_id, supplier_id, name, description, price, sku, unit, img_name, discount) VALUES +(1, 3, 'SmartFeeder One', 'This AI-powered feeder learns your cat''s snack schedule based on nap cycles and mealtime habits. It detects overeating, undernapping, and auto-updates a Feline Health Repo.', 129.99, 'CAT-FEED-001', 'piece', 'feeder.png', 0.25), +(2, 3, 'AutoClean Litter Dome', 'A self-cleaning litter box that detects patterns in your cat''s... commits. Sends you a health report and Slack alert if things look off.', 199.99, 'CAT-LITTER-001', 'piece', 'litter-box.png', 0.25), +(3, 2, 'CatFlix Entertainment Portal', 'On-demand laser shows, motion videos, and bird-watching streams - customized per cat using AI interest tracking. Think Netflix, but for felines.', 89.99, 'CAT-FLIX-001', 'piece', 'catflix.png', 0.0), +(4, 2, 'PawTrack Smart Collar', 'GPS and activity tracker with AI-powered mood detection based on tail position, purring frequency, and movement patterns. Syncs with your phone for walk stats and zoomie alerts.', 79.99, 'CAT-COLLAR-001', 'piece', 'smart-collar.png', 0.0), +(5, 1, 'WhiskerCam Pro', 'Monitor your cat''s daily shenanigans with this 360-degree camera featuring motion alerts, treat dispensing, and live streaming. Includes night vision for midnight mischief detection.', 149.99, 'CAT-CAM-001', 'piece', 'chirp-cam.png', 0.15), +(6, 1, 'ThermoNest Deluxe', 'Self-heating pet bed with temperature sensors that adjust to your cat''s preferred warmth. Includes memory foam and a built-in purr simulator for ultimate comfort.', 99.99, 'CAT-BED-001', 'piece', 'sleep-nest.png', 0.0), +(7, 1, 'ClimbCast Cat Tree', 'Multi-level climbing structure with integrated speakers, charging stations, and modular perches. Designed for modern cats who appreciate both function and style.', 299.99, 'CAT-TREE-001', 'piece', 'scratch-pad.png', 0.10), +(8, 2, 'HydroFlow Smart Bowl', 'AI-powered water fountain that monitors hydration levels and sends health alerts. Features filtration system and customizable flow patterns to entice picky drinkers.', 119.99, 'CAT-WATER-001', 'piece', 'smart-fountain.png', 0.0), +(9, 3, 'PurrFect Groomer Bot', 'Robotic grooming assistant with gentle brushes and nail trimmers. Uses AI to detect grooming preferences and stress levels for a spa-like experience.', 399.99, 'CAT-GROOM-001', 'piece', 'auto-groomer.png', 0.20), +(10, 1, 'MemoryFoam Recovery Pod', 'Therapeutic resting pod with memory foam and heat therapy for senior cats or post-surgery recovery. Includes monitoring sensors for health tracking.', 179.99, 'CAT-POD-001', 'piece', 'snack-vault.png', 0.0), +(11, 3, 'DoorDash Smart Portal', 'Smart cat door with facial recognition and time-based access. Prevents midnight squirrel parties and tracks in/out commits to your dashboard.', 159.99, 'CAT-DOOR-001', 'piece', 'door-dash.png', 0.0), +(12, 2, 'ZoomieTracker AI Mat', 'A motion-sensing mat that detects zoomies, spins up chase lights, and logs agility bursts to a weekly health report. Yes, it graphs zoomies per hour.', 79.99, 'CAT-TRACKER-001', 'piece', 'tracker-mat.png', 0.0); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/architecture.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/architecture.md new file mode 100644 index 000000000..710646e5f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/architecture.md @@ -0,0 +1,83 @@ +# OctoCAT Supply Chain Management System Architecture + +This site is a demo application written in TypeScript. The entire app was created originally from an [ERD diagram](../api-ts/ERD.png) and natural language prompts using Copilot. The frontend was created in the same way, using some of the design ideas in [the design folder](./design/). + +The hero image and product images were created by prompting ChatGPT! + +## Architecture Overview + +The system is a modern supply chain management application built using TypeScript, comprising a backend REST API and a React frontend. It's designed to demonstrate Copilot capabilities using a fairly typical architecture with a little complexity, but not enough to derail demos! + +### Backend Architecture +- Express.js API with RESTful endpoints for all entities +- SQLite persistence using a lightweight repository pattern +- Schema migrations and seed data managed via SQL files +- Swagger/OpenAPI documentation integration +- Entity models with proper relationships following an ERD diagram + +### Frontend Architecture +- React 18+ with TypeScript +- Vite build tool for fast development +- Tailwind CSS for UI styling + +### DevOps Integration +- Docker/Docker Compose for containerization +- Optional Bicep + GitHub Actions plan for Azure deployment + +## ERD + +```mermaid +erDiagram + Headquarters ||--o{ Branch: has + Branch ||--o{ Order: placed_at + Order ||--o{ OrderDetail: contains + OrderDetail ||--o{ OrderDetailDelivery: fulfilled_by + OrderDetail }|--|| Product: references + Delivery ||--o{ OrderDetailDelivery: includes + Supplier ||--o{ Delivery: provides +``` + +## Component Architecture + +```mermaid +flowchart TD + subgraph "Frontend" + F[React App] --> FC[React Components] + FC --> FT[TypeScript] + FC --> FCSS[Tailwind CSS] + F --> FV[Vite Build Tool] + end + + subgraph "Backend" + B[Express.js API] --> BR[REST Routes] + BR --> BM[Entity Models] + B --> BS[Swagger/OpenAPI] + B --> BO[TAO Observability] + B --> R[Repositories] + end + subgraph "Data" + D[(SQLite DB\napi-ts/data/app.db)] + end + + R <--SQL queries--> D + F <--REST API--> B +``` + +## Key Features + +- Complete REST APIs for all supply chain entities +- SQLite-backed persistence with foreign keys and indexed queries +- Declarative migrations (src/database/migrations) and deterministic seed data +- Detailed OpenAPI documentation, generated by Copilot +- Modern React UI with responsive design, generated by Copilot using images +- Containerization for consistent deployment, generated by Copilot + +## Persistence and Data Model + +- Database: SQLite (file located at `api-ts/data/app.db` by default; configurable via `DB_FILE`) +- Access: thin repository layer that maps camelCase models to snake_case columns +- Migrations: SQL scripts in `src/database/migrations` executed in order and tracked in a `migrations` table +- Seeding: ordered SQL scripts in `src/database/seed` to bootstrap demo data +- Test mode: in-memory database (`:memory:`) for fast and isolated tests + +See the dedicated guide: `sqlite-integration.md`. diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/build.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/build.md new file mode 100644 index 000000000..956347401 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/build.md @@ -0,0 +1,169 @@ +# Building and Testing the OctoCAT Supply Chain Application + +This guide provides instructions for building, running, and testing the OctoCAT Supply Chain Management application, which consists of an API component and a React Frontend. + +## Prerequisites + +- Node.js (version 18 or higher) +- npm (latest version recommended) +- Docker/Podman (optional, for containerization) + +## Installation + +1. Clone the repository +2. Install dependencies: + +```bash +make install +``` + +## Building the Application + +### Using Make Commands + +You can build the entire application or its individual components using the following commands: + +```bash +# Build both API and Frontend components +make build + +# Build only the TypeScript API component +make build-ts + +# Build only the Frontend component +make build-frontend +``` + +Alternatively, if you're in the `api-ts/` or `frontend/` directories, you can use npm commands directly: + +```bash +cd api-ts && npm run build +cd frontend && npm run build +``` + +### Database management + +```bash +# Initialize DB schema (runs migrations) +make db-init + +# Seed sample data +make db-seed +``` + +Alternatively, if you're in the `api-ts/` directory, you can use npm commands directly: + +```bash +cd api-ts +npm run db:init +npm run db:seed +``` + +Environment variables: + +- DB_FILE: path to SQLite database file (default: `api-ts/data/app.db`) +- DB_ENABLE_WAL: enable WAL mode (default: true) +- DB_FOREIGN_KEYS: enforce foreign keys (default: true) +- DB_TIMEOUT: busy timeout in ms (default: 30000) + +### Using VS Code Tasks + +VS Code tasks have been configured to streamline the build process: + +1. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) to open the Command Palette +2. Type `Tasks: Run Task` and select it +3. Choose from the following tasks: + - `Build All`: Builds both API and Frontend components + - `Build API`: Builds only the API component + - `Build Frontend`: Builds only the Frontend component + +Alternatively, you can press `Ctrl+Shift+B` (or `Cmd+Shift+B` on macOS) to run the default build task (`Build All`). + +## Running the Application + +### Using Make Commands + +```bash +# Start both API and Frontend in development mode with hot reloading +make dev + +# Start only the TypeScript API in development mode +make dev-api-ts + +# Start only the Frontend in development mode +make dev-frontend + +# Start the application in production mode +make start +``` + +Alternatively, if you're in the `api-ts/` or `frontend/` directories, you can use npm commands directly: + +```bash +cd api-ts && npm run dev +cd frontend && npm run dev +``` + +### Using VS Code Debugger + +1. Open the Debug panel (`Ctrl+Shift+D` or `Cmd+Shift+D` on macOS) +2. Select `Start API & Frontend` from the dropdown menu +3. Click the green play button or press F5 + +This will start both the API and Frontend in development mode with the integrated terminal, allowing you to see the console output. + +## Testing the Application + +### Running Tests + +```bash +# Run all tests +make test + +# Run TypeScript API tests only +make test-ts + +# Run frontend tests only +make test-frontend +``` + +Alternatively, if you're in the `api-ts/` or `frontend/` directories, you can use npm commands directly: + +```bash +cd api-ts && npm run test +cd frontend && npm run test +``` + +### Linting + +```bash +# Run linting checks +make lint +``` + +Alternatively, if you're in the `api-ts/` or `frontend/` directory, you can use npm commands directly: + +```bash +cd frontend && npm run lint +``` + +```bash +cd api-ts && npm run lint +``` + +## Additional Information + +### Port Configuration + +The API runs on port 3000 by default, and the Frontend runs on port 5137. When running in a Codespace environment, ensure that the API port visibility is set to `public` to avoid CORS errors when the Frontend tries to communicate with the API. + +For Docker, the sample compose maps API to 3000 and frontend to 3001. + +### Docker Deployment + +The project includes Dockerfiles for both API and Frontend components and a docker-compose.yml file for easy containerized deployment: + +```bash +# Build and start using Docker Compose +docker-compose up --build +``` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/cart.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/cart.png new file mode 100644 index 000000000..b4ac91ceb Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/cart.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/footer.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/footer.png new file mode 100644 index 000000000..c31b474d9 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/footer.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/item-list.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/item-list.png new file mode 100644 index 000000000..e0f925172 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/item-list.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/main.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/main.png new file mode 100644 index 000000000..9281653cf Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/main.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/order-tracking-prototype.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/order-tracking-prototype.png new file mode 100644 index 000000000..d1ffd956a Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/design/order-tracking-prototype.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/sqlite-integration.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/sqlite-integration.md new file mode 100644 index 000000000..d811e89f5 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/sqlite-integration.md @@ -0,0 +1,268 @@ +# SQLite Database Integration + +This document explains how to use the SQLite database integration in the OctoCAT Supply Chain Management API. + +## Overview + +The API has been migrated from in-memory data storage to a persistent SQLite database. This provides: + +- **Data persistence** - Data survives server restarts +- **ACID transactions** - Reliable data consistency +- **Proper relationships** - Foreign key constraints between entities +- **Performance** - Indexed queries for better performance +- **Testing** - In-memory database for unit tests + +## Database Structure + +The database consists of the following tables: + +- `suppliers` - Supplier information +- `headquarters` - Company headquarters data +- `branches` - Branch locations (linked to headquarters) +- `products` - Product catalog (linked to suppliers) +- `orders` - Customer orders (linked to branches) +- `order_details` - Order line items (linked to orders and products) +- `deliveries` - Delivery tracking (linked to suppliers) +- `order_detail_deliveries` - Junction table for order-delivery relationships +- `migrations` - Database schema version tracking + +## Getting Started + +### 1. Database Initialization + +When you start the server for the first time, the database will be automatically initialized: + +```bash +make dev +``` + +This will: + +- Create the SQLite database file at `api-ts/data/app.db` +- Run all pending migrations +- Seed the database with sample data (if empty) + +### 2. Manual Database Management + +You can also manage the database manually from the root directory: + +```bash +# Initialize database schema (runs migrations) +make db-init + +# Seed the database with sample data +make db-seed +``` + +Alternatively, if you're in the `api-ts/` directory, you can use npm commands directly: + +```bash +cd api-ts +npm run db:init +npm run db:seed +``` + +### 3. Database Location + +The database file is stored at: + +- **Development**: `api-ts/data/app.db` +- **Testing**: In-memory (`:memory:`) + +You can override the database location using the `DB_FILE` environment variable: + +```bash +export DB_FILE=/path/to/your/database.db +``` + +## Repository Pattern + +The API uses the Repository pattern to interact with the database: + +### Using Repositories + +```typescript +import { getSuppliersRepository } from './repositories/suppliersRepo'; + +const repo = await getSuppliersRepository(); + +// Get all suppliers +const suppliers = await repo.findAll(); + +// Get supplier by ID +const supplier = await repo.findById(1); + +// Create new supplier +const newSupplier = await repo.create({ + name: 'New Supplier', + description: 'Description', + contactPerson: 'John Doe', + email: 'john@example.com', + phone: '555-1234' +}); + +// Update supplier +const updated = await repo.update(1, { name: 'Updated Name' }); + +// Delete supplier +await repo.delete(1); + +// Search by name +const results = await repo.findByName('Tech'); +``` + +### Repository Features + +- **Type Safety** - Full TypeScript support +- **Error Handling** - Proper error types (NotFoundError, ValidationError, etc.) +- **SQL Injection Protection** - Parameterized queries +- **Automatic Mapping** - Converts between snake_case (database) and camelCase (JavaScript) + +## Database Schema Management + +### Migrations + +Database schema changes are managed through migration files: + +1. Create a new migration file: `src/database/migrations/002_description.sql` +2. Add your SQL statements +3. Run migrations: `make db-init` (or `cd api-ts && npm run db:migrate`) + +Migration files are executed in order and tracked in the `migrations` table. + +### Seed Data + +Sample data is provided through seed files in `src/database/seed/`: + +- `001_suppliers.sql` - Supplier data +- `002_headquarters.sql` - Headquarters data +- `003_branches.sql` - Branch data +- `004_products.sql` - Product catalog + +## Testing Strategy + +### Unit Tests + +Repositories are tested using mocked database connections: + +```typescript +import { vi } from 'vitest'; +import { SuppliersRepository } from '../repositories/suppliersRepo'; + +// Mock database +const mockDb = { + run: vi.fn(), + get: vi.fn(), + all: vi.fn(), + close: vi.fn() +}; + +// Test repository methods +const repo = new SuppliersRepository(mockDb); +``` + +### Integration Tests + +For integration tests, use an in-memory database: + +```typescript +import { getDatabase } from '../db/sqlite'; + +const db = await getDatabase(true); // true = test mode (in-memory) +``` + +## Configuration + +Database configuration is managed in `api-ts/src/db/config.ts`: + +```typescript +export const DB_CONFIG = { + DB_FILE: process.env.DB_FILE || './data/app.db', + DB_ENGINE: process.env.DB_ENGINE || 'sqlite', + ENABLE_WAL: process.env.DB_ENABLE_WAL !== 'false', + TIMEOUT: parseInt(process.env.DB_TIMEOUT || '30000'), + FOREIGN_KEYS: process.env.DB_FOREIGN_KEYS !== 'false' +}; +``` + +### Environment Variables + +- `DB_FILE` - Database file path (default: `./data/app.db`) +- `DB_ENGINE` - Database engine (default: `sqlite`) +- `DB_ENABLE_WAL` - Enable WAL mode (default: `true`) +- `DB_TIMEOUT` - Connection timeout in ms (default: `30000`) +- `DB_FOREIGN_KEYS` - Enable foreign key constraints (default: `true`) + +## Error Handling + +The system provides specialized error types: + +- `DatabaseError` - General database errors +- `NotFoundError` - Entity not found (404) +- `ValidationError` - Invalid data (400) +- `ConflictError` - Constraint violations (409) + +These errors are automatically handled by the Express error middleware and return appropriate HTTP status codes. + +## Performance Considerations + +The database includes several optimizations: + +- **Indexes** - On foreign keys and frequently queried columns +- **WAL Mode** - Better concurrency for read/write operations +- **Connection Pooling** - Reuses database connections +- **Query Optimization** - Parameterized queries prevent SQL injection + +## Backup and Recovery + +Since SQLite stores data in a single file, backup is straightforward: + +```bash +# Backup database +cp api-ts/data/app.db api-ts/data/app-backup-$(date +%Y%m%d).db + +# Restore from backup +cp api-ts/data/app-backup-20231225.db api-ts/data/app.db +``` + +For production deployments, consider regular automated backups. + +## Troubleshooting + +### Common Issues + +1. **Database locked**: Usually caused by long-running transactions or unclosed connections + - Solution: Ensure all database operations are properly awaited and connections are closed + +2. **Foreign key constraint errors**: Trying to reference non-existent records + - Solution: Ensure referenced records exist before creating relationships + +3. **Migration errors**: SQL syntax errors or conflicting schema changes + - Solution: Check migration file syntax and ensure compatibility with existing schema + +### Debug Mode + +Enable verbose SQLite logging: + +```bash +NODE_ENV=development make dev +``` + +This will show all SQL queries being executed. + +## Next Steps + +The current implementation includes: + +- ✅ Complete SQLite infrastructure +- ✅ Suppliers repository and routes +- ✅ Migration and seeding system +- ✅ Unit tests with mocking + +Still to implement: + +- [ ] Repositories for remaining entities (products, orders, etc.) +- [ ] Route migration for remaining endpoints +- [ ] Integration tests +- [ ] Docker configuration updates +- [ ] Production deployment considerations diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/tao.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/tao.md new file mode 100644 index 000000000..b72b8b4af --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/docs/tao.md @@ -0,0 +1,107 @@ +# TypeScript API Observability Framework (TAO) + +TAO is our lightweight, powerful observability framework designed specifically for TypeScript-based APIs. It provides seamless integration of metrics, traces, and logs with minimal configuration. + +## Features + +- 🔄 Auto-instrumentation for Express.js routes +- 📊 Built-in metrics collection (response times, error rates, request counts) +- 🔍 Distributed tracing with OpenTelemetry compatibility +- 📝 Structured logging with context correlation +- 🎯 Custom metric decorators +- 🔌 Pluggable backend support (Prometheus, Jaeger, ELK) + +## Quick Start + +```typescript +import { initTAO, observe } from '@tao/core'; +import express from 'express'; + +// Initialize TAO with default settings +initTAO({ + serviceName: 'my-api', + environment: 'production' +}); + +const app = express(); + +// Automatically instrument all routes +app.use(observe()); +``` + +## Decorators + +TAO provides decorators for fine-grained observability: + +```typescript +import { Measure, Trace, Log } from '@tao/core'; + +class UserService { + @Measure('user.creation.time') + @Trace('user-creation') + @Log('debug') + async createUser(userData: UserData): Promise { + // Your implementation + } +} +``` + +## Custom Metrics + +```typescript +import { MetricRegistry } from '@tao/core'; + +// Create a custom counter +const requestCounter = MetricRegistry.counter({ + name: 'api_requests_total', + labels: ['endpoint', 'status'] +}); + +// Increment the counter +requestCounter.inc({ endpoint: '/api/users', status: '200' }); +``` + +## Trace Context + +TAO automatically propagates trace context across service boundaries using W3C Trace Context headers: + +```typescript +import { getTraceContext, withContext } from '@tao/core'; + +async function makeDownstreamCall() { + const context = getTraceContext(); + + return await fetch('http://other-service/api', { + headers: withContext(context) + }); +} +``` + +## Configuration + +```typescript +initTAO({ + serviceName: 'my-api', + environment: 'production', + metrics: { + backend: 'prometheus', + endpoint: '/metrics' + }, + tracing: { + backend: 'jaeger', + samplingRate: 0.1 + }, + logging: { + level: 'info', + format: 'json' + } +}); +``` + +## Best Practices + +1. **Use Meaningful Names**: Choose descriptive names for metrics and traces +2. **Add Labels**: Include relevant labels for better filtering and aggregation +3. **Sample Wisely**: Configure appropriate sampling rates for high-traffic services +4. **Correlate Data**: Use trace IDs in logs for better debugging +5. **Monitor Resource Usage**: Enable built-in resource metrics collection \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.dockerignore b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.dockerignore new file mode 100644 index 000000000..e0fab3f70 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.dockerignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Development files +.env* +!.env.example + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Testing +coverage/ +test-results/ +*.tgz + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Documentation +README.md +docs/ +*.md + +# Temporary files +tmp/ +temp/ + +# Build cache +.cache/ +dist/ + +# Source maps +*.map diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.gitignore b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.gitignore new file mode 100644 index 000000000..f01ca5eec --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Playwright +test-results/ +playwright-report/ +playwright/.cache/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/Dockerfile b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/Dockerfile new file mode 100644 index 000000000..5ad4bdf1f --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist/ /usr/share/nginx/html/ +COPY --from=builder /app/public/* /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY entrypoint.sh /entrypoint.sh + +# Set proper permissions for the nginx html directory and entrypoint script +RUN chmod -R 755 /usr/share/nginx/html && \ + chown -R nginx:nginx /usr/share/nginx/html && \ + chmod +x /entrypoint.sh + +# Set default environment variables +ENV API_HOST=api +ENV API_PORT=3000 + +EXPOSE 80 + +# Use ENTRYPOINT instead of CMD for better handling in App Service +ENTRYPOINT ["/entrypoint.sh"] diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/README.md b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/README.md new file mode 100644 index 000000000..40ede56ea --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/entrypoint.sh b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/entrypoint.sh new file mode 100644 index 000000000..3bcabd795 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/entrypoint.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# Debug: Print the initial values received from the environment +echo "Initial API_HOST: $API_HOST" +echo "Initial API_PORT: $API_PORT" +echo "Intial API_PROTOCOL: $API_PROTOCOL" + +# Use environment variables provided by App Service, otherwise use defaults +# The ':=' syntax assigns the default value if the variable is unset or null. +EFFECTIVE_API_HOST=${API_HOST:=api} +EFFECTIVE_API_PORT=${API_PORT:=3000} +EFFECTIVE_API_PROTOCOL=${API_PROTOCOL:=https} + +# Check for Azure App Settings format (APPSETTING_API_HOST) +if [ ! -z "$APPSETTING_API_HOST" ]; then + echo "Found APPSETTING_API_HOST: $APPSETTING_API_HOST" + EFFECTIVE_API_HOST=$APPSETTING_API_HOST +fi + +if [ ! -z "$APPSETTING_API_PORT" ]; then + echo "Found APPSETTING_API_PORT: $APPSETTING_API_PORT" + EFFECTIVE_API_PORT=$APPSETTING_API_PORT +fi + +if [ ! -z "$APPSETTING_API_PROTOCOL" ]; then + echo "Found APPSETTING_API_PROTOCOL: $APPSETTING_API_PROTOCOL" + EFFECTIVE_API_PROTOCOL=$APPSETTING_API_PROTOCOL +fi + +echo "Using API_HOST: $EFFECTIVE_API_HOST" +echo "Using API_PORT: $EFFECTIVE_API_PORT" +echo "Using API_PROTOCOL: $EFFECTIVE_API_PROTOCOL" + +# Export the effective variables so envsubst can use them +export API_HOST=$EFFECTIVE_API_HOST +export API_PORT=$EFFECTIVE_API_PORT +export API_PROTOCOL=$EFFECTIVE_API_PROTOCOL + +# Create runtime config JS file with the current API URL +# Default ports: 80,443 +PORT_SECTION="" +if [ "$EFFECTIVE_API_PORT" != "80" ] && [ "$EFFECTIVE_API_PORT" != "443" ]; then + PORT_SECTION=":${EFFECTIVE_API_PORT}" +fi + +cat > /usr/share/nginx/html/runtime-config.js << EOF +window.RUNTIME_CONFIG = { + API_URL: "${API_PROTOCOL}://${API_HOST}${PORT_SECTION}" +}; +console.log("Runtime config loaded:", window.RUNTIME_CONFIG); +EOF + +# Debug: Show the generated runtime config +echo "Generated runtime config:" +cat /usr/share/nginx/html/runtime-config.js +echo "" + +# Start nginx +echo "Starting nginx..." +exec nginx -g "daemon off;" \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/eslint.config.js b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/eslint.config.js new file mode 100644 index 000000000..9b4edb2c6 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/eslint.config.js @@ -0,0 +1,31 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig } from 'eslint/config'; + +export default defineConfig( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + curly: ['error', 'all'], + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + "for-direction": "off" + }, + }, +) diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/index.html b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/index.html new file mode 100644 index 000000000..affe3c3ed --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + OctoCAT Supply + + + +
+ + + diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/nginx.conf b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/nginx.conf new file mode 100644 index 000000000..c7d26a1e2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name localhost; + + error_log /var/log/nginx/error.log debug; + access_log /var/log/nginx/access.log; + + root /usr/share/nginx/html; + index index.html; + + # Serve static files and single page application + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + autoindex off; + } + + # Specific rule for static files + location ~* \.(png|jpg|jpeg|gif|ico|svg)$ { + root /usr/share/nginx/html; + expires 30d; + add_header Cache-Control "public, no-transform"; + access_log off; + + # Fix for 403 errors + try_files $uri =404; + } +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package-lock.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package-lock.json new file mode 100644 index 000000000..ff4532b9c --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package-lock.json @@ -0,0 +1,5861 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@types/react-slick": "^0.23.13", + "axios": "1.8.1", + "globals": "16.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-query": "^3.39.3", + "react-router-dom": "^7.4.1", + "react-slick": "^0.30.3", + "slick-carousel": "^1.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@playwright/test": "^1.49.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.14.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.14", + "eslint": "^9.37.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "jsdom": "^26.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "7.2.6", + "vitest": "4.0.10" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.52.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-slick": { + "version": "0.23.13", + "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz", + "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.10.tgz", + "integrity": "sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.10.tgz", + "integrity": "sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.10.tgz", + "integrity": "sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.10.tgz", + "integrity": "sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.10.tgz", + "integrity": "sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.10.tgz", + "integrity": "sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.10.tgz", + "integrity": "sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.10", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT", + "peer": true + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "license": "ISC", + "dependencies": { + "big-integer": "^1.6.16" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-slick": { + "version": "0.30.3", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.3.tgz", + "integrity": "sha512-B4x0L9GhkEWUMApeHxr/Ezp2NncpGc+5174R02j+zFiWuYboaq98vmxwlpafZfMjZic1bjdIqqmwLDcQY0QaFA==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", + "license": "MIT", + "peerDependencies": { + "jquery": ">=1.8.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.10.tgz", + "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.10", + "@vitest/mocker": "4.0.10", + "@vitest/pretty-format": "4.0.10", + "@vitest/runner": "4.0.10", + "@vitest/snapshot": "4.0.10", + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.10", + "@vitest/browser-preview": "4.0.10", + "@vitest/browser-webdriverio": "4.0.10", + "@vitest/ui": "4.0.10", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package.json b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package.json new file mode 100644 index 000000000..6a43b4848 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "npm run test:e2e", + "test:e2e": "playwright test" + }, + "dependencies": { + "@types/react-slick": "^0.23.13", + "axios": "1.8.1", + "globals": "16.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-query": "^3.39.3", + "react-router-dom": "^7.4.1", + "react-slick": "^0.30.3", + "slick-carousel": "^1.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.14.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@playwright/test": "^1.49.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.14", + "eslint": "^9.37.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "jsdom": "^26.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "7.2.6", + "vitest": "4.0.10" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.52.4" + } +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/playwright.config.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/playwright.config.ts new file mode 100644 index 000000000..8df9284a5 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright configuration scoped to Chromium only. + */ +export default defineConfig({ + testDir: './tests/e2e', + timeout: 60_000, + expect: { + timeout: 10_000, + }, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5137', + trace: 'on-first-retry', + video: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: + process.env.PLAYWRIGHT_WEB_SERVER !== 'false' + ? { + command: 'cd .. && make dev', + port: 5137, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + } + : undefined, +}); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/postcss.config.js b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/postcss.config.js new file mode 100644 index 000000000..52012d2b5 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + } +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/auto-groomer.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/auto-groomer.png new file mode 100644 index 000000000..d58136498 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/auto-groomer.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/catflix.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/catflix.png new file mode 100644 index 000000000..b67cd701e Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/catflix.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/chirp-cam.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/chirp-cam.png new file mode 100644 index 000000000..d6ee443f9 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/chirp-cam.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/copilot.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/copilot.png new file mode 100644 index 000000000..2eb1aff0a Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/copilot.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/door-dash.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/door-dash.png new file mode 100644 index 000000000..3f9f96164 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/door-dash.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/feeder.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/feeder.png new file mode 100644 index 000000000..49c3dd7b0 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/feeder.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/hero.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/hero.png new file mode 100644 index 000000000..b9c37fa2a Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/hero.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/litter-box.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/litter-box.png new file mode 100644 index 000000000..48668fa72 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/litter-box.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/runtime-config.js b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/runtime-config.js new file mode 100644 index 000000000..f504f36a2 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/runtime-config.js @@ -0,0 +1,5 @@ +(function () { + // In development, the API url is built in api/config.ts + // In production containers, this file is overwritten by `entrypoint.sh`. + window.RUNTIME_CONFIG = Object.assign({}, window.RUNTIME_CONFIG, { API_URL: undefined }); +})(); diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/scratch-pad.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/scratch-pad.png new file mode 100644 index 000000000..9e03a47fd Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/scratch-pad.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/sleep-nest.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/sleep-nest.png new file mode 100644 index 000000000..64eaa1095 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/sleep-nest.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-collar.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-collar.png new file mode 100644 index 000000000..7188dba8b Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-collar.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-fountain.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-fountain.png new file mode 100644 index 000000000..dd94d3dc6 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/smart-fountain.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/snack-vault.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/snack-vault.png new file mode 100644 index 000000000..0c7a58024 Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/snack-vault.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/tracker-mat.png b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/tracker-mat.png new file mode 100644 index 000000000..90b53a9cd Binary files /dev/null and b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/public/tracker-mat.png differ diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/App.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/App.tsx new file mode 100644 index 000000000..29b9f534e --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/App.tsx @@ -0,0 +1,48 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Navigation from './components/Navigation'; +import Welcome from './components/Welcome'; +import About from './components/About'; +import Footer from './components/Footer'; +import Products from './components/entity/product/Products'; +import Login from './components/Login'; +import { AuthProvider } from './context/AuthContext'; +import { ThemeProvider } from './context/ThemeContext'; +import AdminProducts from './components/admin/AdminProducts'; +import { useTheme } from './context/ThemeContext'; + +// Wrapper component to apply theme classes +function ThemedApp() { + const { darkMode } = useTheme(); + + return ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ); +} + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/api/config.ts b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/api/config.ts new file mode 100644 index 000000000..a884407fe --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/api/config.ts @@ -0,0 +1,45 @@ +// Add runtime config type definition +declare global { + interface Window { + RUNTIME_CONFIG?: { + API_URL: string; + }; + } +} + +const getBaseUrl = () => { + // First check runtime configuration (from runtime-config.js) + if (typeof window !== 'undefined' && window.RUNTIME_CONFIG?.API_URL) { + console.log('Using runtime config API_URL:', window.RUNTIME_CONFIG.API_URL); + return window.RUNTIME_CONFIG.API_URL; + } + + // During local dev, make targets set VITE_API_URL to the chosen backend port. + const viteApiUrl = import.meta.env.VITE_API_URL; + if (viteApiUrl) { + console.log(`Using Vite API URL: ${viteApiUrl}`); + return viteApiUrl; + } + + const protocol = typeof window !== 'undefined' ? window.location.protocol : 'https:'; + const protocolToUse = protocol.includes('https') ? 'https' : 'http'; + const url = `${protocolToUse}://localhost:3000`; + console.log(`Using default URL: ${url}`); + return url; +}; + +export const API_BASE_URL = getBaseUrl(); + +export const api = { + baseURL: API_BASE_URL, + endpoints: { + products: '/api/products', + suppliers: '/api/suppliers', + orders: '/api/orders', + branches: '/api/branches', + headquarters: '/api/headquarters', + deliveries: '/api/deliveries', + orderDetails: '/api/order-details', + orderDetailDeliveries: '/api/order-detail-deliveries', + }, +}; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/About.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/About.tsx new file mode 100644 index 000000000..c5d5ea948 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/About.tsx @@ -0,0 +1,61 @@ +import { useTheme } from '../context/ThemeContext'; + +const About = () => { + const { darkMode } = useTheme(); + + return ( +
+
+

+ About OctoCAT Supply +

+
+

+ Welcome to OctoCAT Supply, your premier destination for AI-powered smart products + designed specifically for your feline companions. Our cutting-edge cat tech innovations + bring together the latest in artificial intelligence, sensor technology, and + pet-friendly design to enhance the bond between you and your cat. +

+

Our Meow-ssion

+

+ To revolutionize the way cats and humans interact through thoughtfully designed, + AI-enhanced products that improve feline happiness, health monitoring, and enrichment + while delighting their human companions with valuable insights. +

+

Our Purr-pose

+

+ At OctoCAT Supply, we believe that cats deserve the same technological innovations that + humans enjoy. Our team of feline behavior specialists, engineers, and AI experts work + together to create products that understand, respond to, and improve your cat's daily + life. +

+

Key Features of Our Products

+
    +
  • AI-powered behavior analysis and personalization
  • +
  • Real-time health monitoring and wellness alerts
  • +
  • Multi-cat household compatibility
  • +
  • Smartphone integration with detailed analytics
  • +
  • Energy-efficient and eco-friendly materials
  • +
  • Sleek, modern designs that complement your home
  • +
+
+

+ "Our cats tested every product in our catalog extensively. Only the ones they couldn't + stop using made it to production." — Felix Whiskerton, Founder +

+
+
+
+
+ ); +}; + +export default About; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Footer.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Footer.tsx new file mode 100644 index 000000000..44fa1de02 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Footer.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { useTheme } from '../context/ThemeContext'; + +const Footer: React.FC = () => { + const { darkMode } = useTheme(); + + return ( + + ); +}; + +export default Footer; diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Login.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Login.tsx new file mode 100644 index 000000000..6429c7f52 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Login.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { useTheme } from '../context/ThemeContext'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + const { darkMode } = useTheme(); + const [searchParams] = useSearchParams(); + + useEffect(() => { + const errorMsg = searchParams.get('error'); + if (errorMsg) { + setError(errorMsg); + } + }, [searchParams]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await login(email, password); + navigate('/'); + } catch { + setError('Login failed. Please try again.'); + } + }; + + return ( +
+
+

+ Login +

+ + {error && ( +
+ )} + +
+
+ + setEmail(e.target.value)} + className={`w-full ${darkMode ? 'bg-gray-700 text-light' : 'bg-gray-100 text-gray-800'} rounded px-3 py-2 transition-colors duration-300`} + required + autoFocus + /> +
+ +
+ + setPassword(e.target.value)} + className={`w-full ${darkMode ? 'bg-gray-700 text-light' : 'bg-gray-100 text-gray-800'} rounded px-3 py-2 transition-colors duration-300`} + required + /> +
+ + +
+
+
+ ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Navigation.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Navigation.tsx new file mode 100644 index 000000000..5f35e1290 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Navigation.tsx @@ -0,0 +1,145 @@ +import { Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { useTheme } from '../context/ThemeContext'; +import { useState } from 'react'; + +export default function Navigation() { + const { isLoggedIn, isAdmin, logout } = useAuth(); + const { darkMode, toggleTheme } = useTheme(); + const [adminMenuOpen, setAdminMenuOpen] = useState(false); + + return ( + + ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Welcome.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Welcome.tsx new file mode 100644 index 000000000..1455680d9 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/Welcome.tsx @@ -0,0 +1,434 @@ +import Slider from 'react-slick'; +import { useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTheme } from '../context/ThemeContext'; + +export default function Welcome() { + const sliderRef = useRef(null); + const { darkMode } = useTheme(); + const navigate = useNavigate(); + + const sliderSettings = { + dots: true, + infinite: true, + speed: 500, + slidesToShow: 6, + slidesToScroll: 1, + autoplay: true, + autoplaySpeed: 3000, + responsive: [ + { + breakpoint: 1024, + settings: { + slidesToShow: 4, + }, + }, + { + breakpoint: 768, + settings: { + slidesToShow: 3, + }, + }, + { + breakpoint: 640, + settings: { + slidesToShow: 2, + }, + }, + ], + }; + + return ( +
+ {/* Content */} +
+
+ {/* Hero Image */} +
+ Smart Cat Products powered by AI +
+ + {/* Text Content */} +
+
+
+ + Powered by Advanced AI + +
+

+ Smart Cat Tech. +
+ Purrsonalized. +

+

+ OctoCAT Supply brings cutting-edge AI technology to enhance your cat's life. Our + premium smart products learn from your feline friend's behavior to provide + personalized experiences, health insights, and next-level entertainment. +

+ +
+
+
+ + {/* Partner Logos */} +
+
+ {/* Section Title - Takes 20% width on larger screens */} +
+

+ Trusted By Cat Lovers Everywhere +

+
+ + {/* Carousel - Takes 80% width on larger screens */} +
+ {/* SVG Filter Definition */} + + + + + + + + + + + + + + + {/* Logo 1 - Cat Cafe */} +
+
+ + + + + + + +
+ + Whiskers Cafe + +
+ + {/* Logo 2 - Paw Tech */} +
+
+ + + + + + + + + +
+ + PawTech Solutions + +
+ + {/* Logo 3 - Feline Innovations */} +
+
+ + + + + + +
+ + Feline Innovations + +
+ + {/* Logo 4 - Cat Health AI */} +
+
+ + + + + + + +
+ + CatHealth AI + +
+ + {/* Logo 5 - Purr Tech */} +
+
+ + + + + + + + + +
+ + PurrTech Innovations + +
+ + {/* Logo 6 - Whisker Data */} +
+
+ + + + + + + + +
+ + WhiskerWare Systems + +
+
+
+
+
+ + {/* Product Categories */} +
+

+ Smart Solutions for Modern Cats +

+
+
+
+ + + +
+

+ Smart Monitoring +

+

+ AI-powered devices that track your cat's health, activity, and behavior patterns to + provide valuable insights. +

+
+
+
+ + + + +
+

+ Interactive Entertainment +

+

+ Engaging toys and systems that adapt to your cat's play style and preferences for + maximum enjoyment. +

+
+
+
+ + + +
+

+ Comfort & Wellness +

+

+ Smart beds, feeding solutions, and grooming tools designed to enhance your cat's + health and comfort. +

+
+
+
+
+
+ ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/admin/AdminProducts.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/admin/AdminProducts.tsx new file mode 100644 index 000000000..332caa129 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/admin/AdminProducts.tsx @@ -0,0 +1,283 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '../../context/AuthContext'; +import { useTheme } from '../../context/ThemeContext'; +import { Navigate } from 'react-router-dom'; +import ProductForm from '../entity/product/ProductForm'; +import axios from 'axios'; +import { api } from '../../api/config'; + +interface Supplier { + supplierId: number; + name: string; + description: string; + contactPerson: string; + email: string; + phone: string; +} + +interface Product { + productId: number; + supplierId: number; + name: string; + description: string; + price: number; + sku: string; + unit: string; + imgName: string; + supplier?: Supplier; + discount?: number; +} + +type SortField = 'name' | 'price' | 'sku' | 'unit' | 'supplier'; +type SortOrder = 'asc' | 'desc'; + +export default function AdminProducts() { + const { isAdmin } = useAuth(); + const { darkMode } = useTheme(); + const [products, setProducts] = useState([]); + const [suppliers, setSuppliers] = useState([]); + const [editingProduct, setEditingProduct] = useState(undefined); + const [showForm, setShowForm] = useState(false); + const [sortField, setSortField] = useState('name'); + const [sortOrder, setSortOrder] = useState('asc'); + + useEffect(() => { + fetchProducts(); + fetchSuppliers(); + }, []); + + const fetchProducts = async () => { + try { + const response = await axios.get(`${api.baseURL}${api.endpoints.products}`); + const productsData = response.data; + // Inconsistent loop direction example + const processedProducts = [...productsData]; + // Clear products below threshold (should use i-- but uses i++) + for (let i = 5; i >= 0; i++) { + processedProducts[i] = null; + } + // Fetch supplier details for each product + const productsWithSuppliers = await Promise.all( + productsData.map(async (product: Product) => { + try { + const supplierResponse = await axios.get( + `${api.baseURL}${api.endpoints.suppliers}/${product.supplierId}`, + ); + return { ...product, supplier: supplierResponse.data }; + } catch (error) { + console.error(`Error fetching supplier for product ${product.productId}:`, error); + return product; + } + }), + ); + + setProducts(productsWithSuppliers); + } catch (error) { + console.error('Error fetching products:', error); + } + }; + + const fetchSuppliers = async () => { + try { + const response = await axios.get(`${api.baseURL}${api.endpoints.suppliers}`); + setSuppliers(response.data); + } catch (error) { + console.error('Error fetching suppliers:', error); + } + }; + + const handleSort = (field: SortField) => { + if (field === sortField) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('asc'); + } + }; + + const sortedProducts = [...products].sort((a, b) => { + const modifier = sortOrder === 'asc' ? 1 : -1; + if (sortField === 'price') { + return (a[sortField] - b[sortField]) * modifier; + } + if (sortField === 'supplier') { + return (a.supplier?.name || '').localeCompare(b.supplier?.name || '') * modifier; + } + return a[sortField].localeCompare(b[sortField]) * modifier; + }); + + const renderSortIcon = (field: SortField) => { + if (field !== sortField) { + return '↕'; + } + return sortOrder === 'asc' ? '↑' : '↓'; + }; + + if (!isAdmin) { + return ; + } + + return ( +
+
+

+ Product Management +

+ +
+ +
+ + + + + + + + + + + + + + + {sortedProducts.map((product) => ( + + + + + + + + + + + ))} + +
handleSort('name')} + > + Name {renderSortIcon('name')} + handleSort('supplier')} + > + Supplier {renderSortIcon('supplier')} + handleSort('price')} + > + Price {renderSortIcon('price')} + handleSort('sku')} + > + SKU {renderSortIcon('sku')} + handleSort('unit')} + > + Unit {renderSortIcon('unit')} + + Discount + + Description + + Actions +
+ {product.name} + + {product.supplier?.name || 'Unknown'} + + ${product.price.toFixed(2)} + + {product.sku} + + {product.unit} + + {product.discount ? `${(product.discount * 100).toFixed(0)}%` : '-'} + +
{product.description}
+
+ + +
+
+ + {showForm && ( + setShowForm(false)} + onSave={fetchProducts} + /> + )} +
+ ); +} diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/entity/product/ProductForm.tsx b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/entity/product/ProductForm.tsx new file mode 100644 index 000000000..da7f9a640 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/src/frontend/src/components/entity/product/ProductForm.tsx @@ -0,0 +1,221 @@ +import { useState } from 'react'; +import axios from 'axios'; +import { api } from '../../../api/config'; +import { useTheme } from '../../../context/ThemeContext'; + +interface Supplier { + supplierId: number; + name: string; +} + +interface Product { + productId: number; + supplierId: number; + name: string; + description: string; + price: number; + sku: string; + unit: string; + imgName: string; + discount?: number; +} + +interface ProductFormProps { + product?: Product; + suppliers: Supplier[]; + onClose: () => void; + onSave: () => void; +} + +export default function ProductForm({ product, suppliers, onClose, onSave }: ProductFormProps) { + const { darkMode } = useTheme(); + + const [formData, setFormData] = useState>( + product || { + name: '', + description: '', + price: 0, + sku: '', + unit: '', + supplierId: suppliers[0]?.supplierId || 0, + imgName: '', + }, + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (product) { + await axios.put(`${api.baseURL}${api.endpoints.products}/${product.productId}`, formData); + } else { + await axios.post(`${api.baseURL}${api.endpoints.products}`, formData); + } + if (typeof onSave === 'function') { + onSave(); + } + if (typeof onClose === 'function') { + onClose(); + } + } catch (error) { + console.error('Error saving product:', error); + } + }; + + return ( +
+
+

+ {product ? 'Edit Product' : 'Add New Product'} +

+
+
+ + setFormData({ ...formData, name: e.target.value })} + className={`w-full px-3 py-2 ${darkMode ? 'bg-gray-700 text-light' : 'bg-gray-100 text-gray-800'} rounded transition-colors duration-300`} + required + /> +
+
+ +