diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..4874e06
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,67 @@
+# This file defines path-specific settings for Git, such as line ending
+# normalization, binary file handling, and GitHub-specific display options.
+#
+# https://git-scm.com/docs/gitattributes
+
+# Handle line endings automatically for files detected as text
+# and ensure that all checked-in text files have the LF newline character.
+* text=auto
+
+# Common source files
+*.js text
+*.jsx text
+*.ts text
+*.tsx text
+*.json text
+*.html text
+*.css text
+*.scss text
+*.md text
+*.yml text
+*.yaml text
+*.xml text
+
+# Force LF for shell scripts and configuration files (Unix-style)
+*.sh text eol=lf
+*.bash text eol=lf
+.htaccess text eol=lf
+Makefile text eol=lf
+
+# Force CRLF for Windows batch files
+*.bat text eol=crlf
+*.cmd text eol=crlf
+
+# Denote all files that are truly binary and should not be modified by Git.
+# Images
+*.jpg binary
+*.png binary
+*.gif binary
+*.ico binary
+*.webp binary
+
+# Fonts
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.eot binary
+
+# Documents
+*.pdf binary
+
+# Archives
+*.zip binary
+*.tar binary
+*.gz binary
+
+# These settings affect how GitHub calculates language statistics and searches.
+dist/* linguist-vendored
+build/* linguist-vendored
+docs/* linguist-documentation
+vendor/* linguist-vendored
+test/* linguist-vendored
+
+# Treat lock files as binary during merge to prevent conflicts.
+# These should be managed by their respective package managers.
+package-lock.json merge=binary
+yarn.lock merge=binary
+pnpm-lock.yaml merge=binary
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index fe91b77..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# To get started with Dependabot version updates, you'll need to specify which
-# package ecosystems to update and where the package manifests are located.
-# Please see the documentation for all configuration options:
-# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
-
-version: 2
-updates:
- - package-ecosystem: "npm"
- directory: "/"
- schedule:
- interval: "daily"
- open-pull-requests-limit: 10
-
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
- open-pull-requests-limit: 10
diff --git a/.gitignore b/.gitignore
index 22b2985..9ae1949 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,36 @@
-# Logs
-logs
+# This file specifies intentionally untracked files that Git should ignore.
+# It helps keep the repository clean and prevents sensitive or temporary
+# files from being committed.
+#
+# Reference: https://git-scm.com/docs/gitignore
+
+# Ignore dependency directories and non-essential lock files
+node_modules/
+yarn.lock
+pnpm-lock.yaml
+# Note: package-lock.json is NOT ignored to ensure consistent builds
+# package-lock.json
+
+# Compilation results, build artifacts, and TypeScript cache
+dist/
+build/
+out/
+compiled/
+compiled_app_output/
+*.tsbuildinfo
+
+# Test coverage reports and temporary test framework files
+coverage/
+.coverage
+.nyc_output/
+*.junit.xml
+junit.xml
+test-results.json
+test-results/
+.vitest-reports/
+
+# Various log files from package managers, build tools, and runtimes
+logs/
*.log
npm-debug.log*
yarn-debug.log*
@@ -7,18 +38,73 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*storybook.log
+build-storybook.log
+build.log
-node_modules
-build
-dist
-dist-ssr
-*.local
+# Configuration files for popular IDEs and editors
-# Editor directories and files
-.DS_Store
+# JetBrains (WebStorm, IntelliJ IDEA, etc.)
+.idea/
+*.iws
+*.iml
+*.ipr
+
+# VS Code
+.vscode/*
+!.vscode/extensions.json
+!.vscode/settings.default.json
+!.vscode/launch.json
+
+# Sublime Text
+*.sublime-project
+*.sublime-workspace
+
+# Other temporary editor files
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+*.swp
+*.bak
+.editorconfig
+.prettierignore
+.eslintignore
+
+# Environment variables, access tokens, and private keys
+.env
+.env.*
+!.env.example
+.secrets/
+google-generated-credentials.json
+.clasp.json
+creds.json
+*.pem
+*.key
+
+# OS-specific files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Various temporary data and application cache
+.tmp/
+tmp/
+.data/
+.cache/
+.eslintcache
+.stylelintcache
+.npm
+.pnpm-store
+# Project-specific reports and temporary files
+*.0x
+nodelinter.config.json
+CHANGELOG-*.md
+*.mdx
+trivy_report*
+!/.gitignore
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 996d906..7354b58 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
index 0c83ac4..a8ff823 100644
--- a/.idea/prettier.xml
+++ b/.idea/prettier.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/.idea/scopes/appsscript.xml b/.idea/scopes/appsscript.xml
index a115fd7..2d5560d 100644
--- a/.idea/scopes/appsscript.xml
+++ b/.idea/scopes/appsscript.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/.idea/scopes/stories.xml b/.idea/scopes/stories.xml
index 0f9ab89..16d1767 100644
--- a/.idea/scopes/stories.xml
+++ b/.idea/scopes/stories.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/.idea/scopes/webapp.xml b/.idea/scopes/webapp.xml
index 75d0ae8..8d8034d 100644
--- a/.idea/scopes/webapp.xml
+++ b/.idea/scopes/webapp.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/.idea/webResources.xml b/.idea/webResources.xml
deleted file mode 100644
index 3432ba4..0000000
--- a/.idea/webResources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
new file mode 100644
index 0000000..2d6e414
--- /dev/null
+++ b/.junie/guidelines.md
@@ -0,0 +1,95 @@
+### Build and Configuration Instructions
+
+#### Prerequisites
+
+- Node.js (latest LTS recommended)
+- npm (installed with Node.js)
+
+#### Installation
+
+Install project dependencies using:
+
+```bash
+npm install
+```
+
+#### Build
+
+The project uses the TypeScript compiler (`tsc`) for building. To compile the source code:
+
+```bash
+npm run build
+```
+
+The output will be located in the `dist` directory.
+
+#### Code Quality
+
+- **Formatting**: Uses Prettier. Run `npm run format` to format the codebase.
+- **Linting**: Uses ESLint. Run `npm run lint` to check for and fix linting issues.
+
+---
+
+### Testing Information
+
+#### Configuration
+
+The project uses [Vitest](https://vitest.dev/) for testing. Configuration is defined in `vitest.config.ts`.
+Important: The project relies on `reflect-metadata`, which must be imported at the beginning of test files or via a
+setup file.
+
+#### Running Tests
+
+- **All tests**: `npm run test`
+- **Watch mode**: `npm run dev`
+- **Specific test file**: `npx vitest run path/to/test.ts`
+
+#### Adding New Tests
+
+1. Create a `.test.ts` file in the `test` directory (or subdirectories mirroring `src`).
+2. Ensure `import "reflect-metadata";` is at the top of the file if using decorators or metadata.
+3. Use `describe`, `it`, and `expect` from `vitest`.
+
+#### Test Example
+
+```typescript
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { HttpController } from "../src/decorators";
+import { CONTROLLER_WATERMARK } from "../src/config/constants";
+
+describe("Example Test", () => {
+ it("should verify controller metadata", () => {
+ @HttpController("/api")
+ class MyController {}
+
+ const isController = Reflect.getMetadata(CONTROLLER_WATERMARK, MyController);
+ expect(isController).toBe(true);
+ });
+});
+```
+
+---
+
+### Additional Development Information
+
+#### Code Style
+
+- Follow the existing NestJS-like architecture.
+- Use decorators for defining controllers, routes, and dependency injection.
+- Prefer explicit type definitions where possible.
+- Use `reflect-metadata` for any custom metadata handling.
+
+#### Key Directories
+
+- `src/`: Source code.
+ - `decorators/`: Custom decorators for controllers and methods.
+ - `types/`: TypeScript type definitions and interfaces.
+ - `utils/`: Internal utility functions for routing and metadata.
+- `test/`: Test suites mirroring the `src` structure.
+
+#### Dependency Management
+
+The project has a dependency on `apps-script-utils` from a GitHub repository:
+`"apps-script-utils": "github:MaksymStoianov/apps-script-utils#main"`
+Ensure you have network access to GitHub when installing dependencies.
diff --git a/.prettierignore b/.prettierignore
index 1e6b49f..7f59c27 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,12 +1,39 @@
-# Ignore artifacts:
-dist
-build
-coverage
+# This file specifies which files and directories should be ignored by Prettier.
+# It helps prevent formatting of generated files, dependencies, and artifacts.
+#
+# Reference: https://prettier.io/docs/en/ignore.html
-**/.git
-**/.svn
-**/.hg
-**/node_modules
+# Ignore compilation results and build artifacts
+dist/
+build/
+out/
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+# Ignore external libraries and modules
+node_modules/
+vendor/
+
+# Ignore test coverage reports
+coverage/
+.coverage
+.nyc_output/
+
+# Ignore VCS-specific files
+.git/
+.svn/
+.hg/
+
+# Ignore files that shouldn't be formatted to maintain their original style
+CHANGELOG.md
+LICENSE
+CODE_OF_CONDUCT.md
+.github/
+
+# Minified files and project-specific ignores
*.min.js
-**/output.css
\ No newline at end of file
+*.min.css
+**/output.css
+.eslintcache
+.stylelintcache!/.prettierignore
diff --git a/.prettierrc.cjs b/.prettierrc.cjs
new file mode 100644
index 0000000..70ad68f
--- /dev/null
+++ b/.prettierrc.cjs
@@ -0,0 +1,56 @@
+module.exports = {
+ /**
+ * https://prettier.io/docs/options.html#print-width
+ */
+ printWidth: 100,
+
+ /**
+ * https://prettier.io/docs/en/options.html#tab-width
+ */
+ tabWidth: 2,
+
+ /**
+ * https://prettier.io/docs/en/options.html#tabs
+ */
+ useTabs: false,
+
+ /**
+ * https://prettier.io/docs/en/options.html#semicolons
+ */
+ semi: true,
+
+ /**
+ * https://prettier.io/docs/en/options.html#quotes
+ */
+ singleQuote: false,
+
+ /**
+ * https://prettier.io/docs/options.html#quote-props
+ */
+ quoteProps: "consistent",
+
+ /**
+ * https://prettier.io/docs/en/options.html#trailing-commas
+ */
+ trailingComma: "none",
+
+ /**
+ * https://prettier.io/docs/en/options.html#bracket-spacing
+ */
+ bracketSpacing: true,
+
+ /**
+ * https://prettier.io/docs/en/options.html#arrow-function-parentheses
+ */
+ arrowParens: "always",
+
+ /**
+ * https://prettier.io/docs/options.html#prose-wrap
+ */
+ proseWrap: "preserve",
+
+ /**
+ * https://prettier.io/docs/options.html#end-of-line
+ */
+ endOfLine: "lf"
+};
diff --git a/.prettierrc.json b/.prettierrc.json
deleted file mode 100644
index d295880..0000000
--- a/.prettierrc.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "singleQuote": false,
- "tabWidth": 2,
- "semi": true,
- "arrowParens": "avoid",
- "trailingComma": "none",
- "quoteProps": "consistent",
- "bracketSpacing": true,
- "proseWrap": "preserve"
-}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..61cb4a4
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at stoianov.maksym+bootgs@gmail.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..42f7b0d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,130 @@
+# Contributing to Boot.gs
+
+Great that you are here and you want to contribute to **Boot.gs**!
+
+## Contents
+
+
+
+- [Contributing to Boot.gs](#contributing-to-bootgs)
+ - [Contents](#contents)
+ - [Code of Conduct](#code-of-conduct)
+ - [Directory Structure](#directory-structure)
+ - [Development Setup](#development-setup)
+ - [Requirements](#requirements)
+ - [Node.js](#nodejs)
+ - [npm](#npm)
+ - [Actual Setup](#actual-setup)
+ - [Development Cycle](#development-cycle)
+ - [Basic Development Workflow](#basic-development-workflow)
+ - [Community PR Guidelines](#community-pr-guidelines)
+ - [1. General Requirements](#1-general-requirements)
+ - [2. PR Specific Requirements](#2-pr-specific-requirements)
+ - [Test Suite](#test-suite)
+ - [Roadmap](#roadmap)
+ - [License](#license)
+
+
+
+## Code of Conduct
+
+This project and everyone participating in it are governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By
+participating, you are expected to uphold this code. Please report unacceptable behavior
+to [stoianov.maksym+bootgs@gmail.com](mailto:stoianov.maksym+bootgs@gmail.com).
+
+## Directory Structure
+
+The project is organized as follows:
+
+- [**`config/`**](config/) — Configuration files for build tools and environment.
+- [**`docs/`**](docs/) — Documentation and assets.
+- [**`scripts/`**](scripts/) — Utility scripts for maintenance and development.
+- [**`src/`**](src/) — Source code of the framework.
+- [**`test/`**](test/) — Test suites mirroring the `src` structure.
+
+## Development Setup
+
+### Requirements
+
+#### Node.js
+
+[Node.js](https://nodejs.org/en/) (latest LTS recommended) is required for development.
+
+#### npm
+
+[npm](https://www.npmjs.com/) is used for dependency management and scripts.
+
+### Actual Setup
+
+1. **Fork** the repository on GitHub.
+2. **Clone** your fork locally:
+ ```bash
+ git clone https://github.com//boot.git
+ cd boot
+ ```
+3. **Add upstream remote**:
+ ```bash
+ git remote add upstream https://github.com/bootgs/boot.git
+ ```
+4. **Install dependencies**:
+ ```bash
+ npm install
+ ```
+5. **Build the project**:
+ ```bash
+ npm run build
+ ```
+
+## Development Cycle
+
+### Basic Development Workflow
+
+1. Hack, hack, hack.
+2. Ensure code quality:
+ ```bash
+ npm run lint
+ npm run format
+ ```
+3. Run [tests](#test-suite):
+ ```bash
+ npm run test
+ ```
+4. Commit your changes and create a Pull Request.
+
+### Community PR Guidelines
+
+#### 1. General Requirements
+
+- **Follow the Style Guide**: Adhere to the project's coding standards (TypeScript, decorators, DI).
+- **TypeScript Compliance**: No `ts-ignore` unless absolutely necessary (and documented).
+- **Avoid Repetitive Code**: Reuse existing utilities and patterns.
+- **Testing**: PRs **must include tests**.
+- **Typos**: Use a spell-checker to avoid typos in code and documentation.
+
+#### 2. PR Specific Requirements
+
+- **Small PRs**: Focus on a single feature or fix per PR.
+- **Naming**: Use clear and descriptive PR titles.
+
+### Test Suite
+
+The project uses [Vitest](https://vitest.dev/) for testing.
+
+- **Run all tests**:
+ ```bash
+ npm run test
+ ```
+- **Watch mode**:
+ ```bash
+ npm run dev
+ ```
+
+## Roadmap
+
+The current vision for the project's development can be found in the [Roadmap](ROADMAP.md). Please note that the
+roadmap is for informational purposes and is subject to change.
+
+## License
+
+By contributing to **Boot.gs**, you agree that your contributions will be licensed under
+the [Apache-2.0 License](LICENSE).
diff --git a/README.md b/README.md
index f2c560a..b94a3d1 100644
--- a/README.md
+++ b/README.md
@@ -8,178 +8,94 @@
-# Boot Framework for Google Apps Script™ projects
-
-[](https://github.com/google/clasp)
-[](LICENSE)
-[](https://github.com/bootgs/boot/releases)
+# Boot Framework for Google Apps Script™
+
+
## Introduction
-**Boot.gs** is a powerful, scalable, and modern framework for building high-performance Google Apps Script applications.
-
-## How to Install
-
-To get started, install the dependencies:
-
-```bash
-npm install github:bootgs/boot#main
-```
+**Boot.gs** is a lightweight framework designed to help build structured Google Apps Script applications. It aims to
+bring familiar development patterns, such as decorators and dependency injection, to the Apps Script environment to
+aid in code organization.
-> **Note:** It's recommended to use tags (`#vX.Y.Z`) for production environments to ensure version stability.
+## Installation
-For example:
+Install the framework via npm:
```bash
-npm install github:bootgs/boot#v1.1.0
+npm install bootgs
```
-## How to Use
+## Quick Start
-### 1. Creating a Controller
+### 1. Define a Controller
-Define a REST controller that will handle HTTP requests to your Apps Script web application:
+Create a class to handle your application's logic. Decorators make it easy to map methods to specific endpoints or
+events.
```TypeScript
-import {Get, RestController} from "boot";
+import {Get, RestController} from "bootgs";
@RestController("api/sheet")
-export class Sheet {
- @Get("active-range")
- getActiveRange(): string {
- return "This action return active range.";
- }
+export class SheetController {
+ /**
+ * Handles GET requests to /api/sheet/active-range
+ */
+ @Get("active-range")
+ getActiveRange(): string {
+ return "This action returns the active sheet range.";
+ }
}
```
-This code is a great example of using the **Boot.gs** framework to build a **web app on Google Apps Script** with
-a **REST API** architecture. Essentially, it turns your standard Apps Script code into a full-fledged web application
-that can handle and respond to HTTP requests (GET and POST).
-
-More ...
-
-#### Why is this needed?
-
-Google Apps Script has simple `doGet` and `doPost` functions for handling web requests, but they're quite basic. To
-build a
-more complex application with multiple API endpoints, you'd have to write a lot of manual routing logic. This can
-quickly become clunky and difficult to manage.
+### 2. Initialize the Application
-**Boot.gs** solves this problem by providing decorators (`@RestController`, `@Get`) and automated routing. This
-allows
-you to structure your code in a way that is common in modern web frameworks like Express.js or NestJS.
-
-The result is code that is more organized, readable, and scalable.
+Bootstrap your application by creating an `App` instance and delegating the standard Apps Script entry points (`doGet`,
+`doPost`) to it.
```TypeScript
-import {App, Newable, Get, RestController} from "boot";
+import {App} from "bootgs";
+import {SheetController} from "./SheetController";
/**
- * This JSDoc comment describes the `doGet` function.
- * It's the standard handler for GET requests in Google Apps Script.
- *
- * @param event The GET request event object, containing request information.
- * @returns The result of the request processing, usually HTML content or JSON.
+ * Global entry point for GET requests.
*/
-// The `doGet` function is a mandatory entry point for web app GET requests.
export function doGet(event: GoogleAppsScript.Events.DoGet) {
- // Defines an array of controllers that the application will use.
- // In this case, only the `Sheet` class is used.
- const controllers: Newable[] = [Sheet];
-
- // Defines an array of providers (services) that will be available for injection.
- // There are no providers in this example.
- const providers: Newable[] = [];
-
- // Creates an application instance, passing it the list of controllers and providers.
- const app = App.create({
- controllers,
- providers
- });
-
- // Delegates the processing of the GET request to the created application instance.
- return app.doGet(event);
+ const app = App.create({
+ controllers: [SheetController]
+ });
+ return app.doGet(event);
}
/**
- * This JSDoc comment describes the `doPost` function.
- * It's the standard handler for POST requests in Google Apps Script.
- *
- * @param event The POST request event object.
- * @returns The result of the request processing.
+ * Global entry point for POST requests.
*/
-// The `doPost` function is the entry point for web app POST requests.
export function doPost(event: GoogleAppsScript.Events.DoPost) {
- // Defines controllers for POST requests (same logic as for `doGet`).
- const controllers: Newable[] = [Sheet];
-
- // Defines providers (none here).
- const providers: Newable[] = [];
-
- // Creates an application instance.
- const app = App.create({
- controllers,
- providers
- });
-
- // Delegates the processing of the POST request to the application.
- return app.doPost(event);
-}
-
-/**
- * This JSDoc comment describes the `Sheet` class.
- * It acts as a REST controller for handling API requests.
- */
-// The `@RestController` decorator declares this class as a controller and sets the base path to "api/sheet".
-@RestController("api/sheet")
-export class Sheet {
- /**
- * This JSDoc comment describes the `getActiveRange` method.
- * It is a handler for a GET request.
- */
- // The `@Get` decorator marks this method as a GET request handler and sets the endpoint path to "active-range".
- // The full path to this endpoint will be "api/sheet/active-range".
- @Get("active-range")
- // The method signature: it takes no arguments and returns a string.
- getActiveRange(): string {
- // The return value of the method.
- return "This action return active range.";
- }
+ const app = App.create({
+ controllers: [SheetController]
+ });
+ return app.doPost(event);
}
```
-#### How does it work?
-
-The process is built on two key parts:
+## Features
-1. Entry Points (`doGet` and `doPost`): These are the only functions that Google Apps Script calls when it receives a
- web
- request. Instead of processing requests directly, they act as a "gateway." They initialize the application (
- `App.create`)
- with its controllers and then delegate all further processing to the framework's core handler.
-
-2. Controllers (`@RestController`): The Sheet class is your controller. The `@RestController("api/sheet")` decorator
- tells
- the
- framework that this class will handle all requests starting with the `/api/sheet` path. The methods within this
- class (
- `getActiveRange`) become your endpoints. The `@Get("active-range")` decorator specifies that the method should handle
- GET
- requests to the `/api/sheet/active-range` path.
-
-So, when a GET request comes in for `https://script.google.com/macros/s/.../exec?path=/api/sheet/active-range`, Apps
-Script
-calls `doGet`, which then passes the request to the framework. The framework parses the URL, finds the matching
-controller (`Sheet`) and method (`getActiveRange`), executes it, and returns the result as an HTTP response.
-
-This approach completely separates your request handling logic from the low-level details of Apps Script, making your
-code **clean**, **modular**, and **maintainable**.
-
-
+- **Decorator-based Routing**: Intuitive mapping of HTTP and Apps Script events.
+- **Dependency Injection**: Decouple your components for better testability.
+- **Type Safety**: Built with TypeScript for a robust development experience.
+- **Modern Architecture**: Inspired by frameworks like NestJS and Spring Boot.
## Decorators
@@ -187,22 +103,90 @@ code **clean**, **modular**, and **maintainable**.
Class decorators
-| Decorator | Description |
-| --------------------- | ------------------------------------------------------------- |
-| `@Controller()` | Marks a class as a general-purpose controller. |
-| `@Service()` | Marks a class as a service, typically holding business logic. |
-| `@Repository()` | Marks a class as a repository, abstracting data access logic. |
-| `@Injectable()` | Marks a class as available for dependency injection. |
-| `@HttpController()` | Marks a class as an HTTP request controller. |
-| `@RestController()` | Alias for `@HttpController()`. |
-| `@DocController()` | Marks a class as a Google Docs event controller. |
-| `@DocsController()` | Alias for `@DocController()`. |
-| `@FormController()` | Marks a class as a Google Forms event controller. |
-| `@FormsController()` | Alias for `@FormController()`. |
-| `@SheetController()` | Marks a class as a Google Sheets event controller. |
-| `@SheetsController()` | Alias for `@SheetController()`. |
-| `@SlideController()` | Marks a class as a Google Slides event controller. |
-| `@SlidesController()` | Alias for `@SlideController()`. |
+
+
+
+
Decorator
+
Returns
+
Description
+
+
+
+
+
@Controller(type?: string, options?: object)
+
ClassDecorator
+
Marks a class as a general-purpose controller.
+
+
+
@HttpController(basePath?: string)
+
ClassDecorator
+
Marks a class as an HTTP request controller. Default base path is /.
@@ -210,28 +194,123 @@ code **clean**, **modular**, and **maintainable**.
Method decorators
-| Decorator | Description |
-| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `@Install()` | Handles [`onInstall`](https://developers.google.com/apps-script/guides/triggers#oninstalle) event, triggered when the add-on is first installed. |
-| `@Open()` | Handles [`onOpen`](https://developers.google.com/apps-script/guides/triggers#onopene) event, triggered when a Google Sheet is opened. |
-| `@Edit()` | Handles [`onEdit`](https://developers.google.com/apps-script/guides/triggers#onedite) event, triggered by manual cell changes in a Google Sheet. |
-| `@Change()` | Handles `onChange` event, triggered by any structural or content change in a Google Sheet. |
-| `@SelectionChange()` | Handles [`onSelectionChange`](https://developers.google.com/apps-script/guides/triggers#onselectionchangee) event, triggered by user cell selection changes. |
-| `@FormSubmit()` | Handles `onFormSubmit` event, triggered when a form connected to a Google Sheet is submitted. |
-| `@Post()` | Maps a method to handle HTTP POST requests. |
-| `@Get()` | Maps a method to handle HTTP GET requests. |
-| `@Delete()` | Maps a method to handle HTTP DELETE requests. |
-| `@Put()` | Maps a method to handle HTTP PUT requests. |
-| `@Patch()` | Maps a method to handle HTTP PATCH requests. |
-| `@Options()` | Maps a method to handle HTTP OPTIONS requests. |
-| `@Head()` | Maps a method to handle HTTP HEAD requests. |
-| `@PostMapping()` | Alias for `@Post()`. |
-| `@GetMapping()` | Alias for `@Get()`. |
-| `@DeleteMapping()` | Alias for `@Delete()`. |
-| `@PutMapping()` | Alias for `@Put()`. |
-| `@PatchMapping()` | Alias for `@Patch()`. |
-| `@OptionsMapping()` | Alias for `@Options()`. |
-| `@HeadMapping()` | Alias for `@Head()`. |
+
Handles onFormSubmit event. Filter by one or more form IDs.
+
+
+
HTTP Methods
+
+
+
@Get(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP GET requests. Default path is /.
+
+
+
@Post(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP POST requests.
+
+
+
@Put(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP PUT requests.
+
+
+
@Patch(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP PATCH requests.
+
+
+
@Delete(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP DELETE requests.
+
+
+
@Head(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP HEAD requests.
+
+
+
@Options(path?: string)
+
MethodDecorator
+
Maps a method to handle HTTP OPTIONS requests.
+
+
+
Aliases
+
+
+
@GetMapping(path?: string)
+
MethodDecorator
+
Alias for @Get().
+
+
+
@PostMapping(path?: string)
+
MethodDecorator
+
Alias for @Post().
+
+
+
@PutMapping(path?: string)
+
MethodDecorator
+
Alias for @Put().
+
+
+
@PatchMapping(path?: string)
+
MethodDecorator
+
Alias for @Patch().
+
+
+
@DeleteMapping(path?: string)
+
MethodDecorator
+
Alias for @Delete().
+
+
+
@HeadMapping(path?: string)
+
MethodDecorator
+
Alias for @Head().
+
+
+
@OptionsMapping(path?: string)
+
MethodDecorator
+
Alias for @Options().
+
+
+
@@ -239,38 +318,92 @@ code **clean**, **modular**, and **maintainable**.
Parameter decorators
-| Decorator | Description |
-| ----------------- | ---------------------------------------------------------- |
-| `@Event()` | Injects the full Google Apps Script event object. |
-| `@Request()` | Injects the full request object or a specific key from it. |
-| `@Headers()` | Injects request headers or a specific header value. |
-| `@Param()` | Injects values from URL path parameters. |
-| `@PathVariable()` | Alias for `@Param()`. |
-| `@Query()` | Injects values from URL query parameters. |
-| `@RequestParam()` | Alias for `@Query()`. |
-| `@Body()` | Injects the full request body or a specific key from it. |
-| `@RequestBody()` | Alias for `@Body()`. |
-| `@Inject()` | Explicitly specifies an injection token for a dependency. |
+
+
+
+
Decorator
+
Returns
+
Description
+
+
+
+
+
@Event()
+
ParameterDecorator
+
Injects the full Google Apps Script event object.
+
+
+
@Request(key?: string)
+
ParameterDecorator
+
Injects the full request object or a specific property.
+
+
+
@Headers(key?: string)
+
ParameterDecorator
+
Injects request headers or a specific header value.
+
+
+
@Body(key?: string)
+
ParameterDecorator
+
Injects the full request body or a specific key.
+
+
+
@Param(key?: string)
+
ParameterDecorator
+
Injects values from URL path parameters.
+
+
+
@Query(key?: string)
+
ParameterDecorator
+
Injects values from URL query parameters.
+
+
+
@Inject(token: any)
+
ParameterDecorator
+
Explicitly specifies an injection token for a dependency.
+
+
+
Aliases
+
+
+
@RequestBody(key?: string)
+
ParameterDecorator
+
Alias for @Body().
+
+
+
@PathVariable(key?: string)
+
ParameterDecorator
+
Alias for @Param().
+
+
+
@RequestParam(key?: string)
+
ParameterDecorator
+
Alias for @Query().
+
+
+
-## Tasks
+## Contributing
-More
+We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct,
+and the process for submitting pull requests.
-- [ ] Develop a `Cron` decorator for methods.
-- [ ] Develop a `Response` decorator for parameters.
+## Roadmap
-
+Check out our [Roadmap](ROADMAP.md) to see what we have planned for future releases.
## Changelog
-For a detailed list of changes and updates, please refer to the [CHANGELOG](CHANGELOG.md) file.
+For a detailed list of changes and updates, please refer to the [CHANGELOG](CHANGELOG.md).
## License
-This project is licensed under the [LICENSE](LICENSE) file.
+This project is licensed under the [Apache-2.0 License](LICENSE).
---
-⭐ **Like this project?** [Star our awesome repo »](https://github.com/bootgs/boot)
+
+ ⭐ Like this project? Give it a star on GitHub!
+
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..cb66ae1
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,49 @@
+# Roadmap
+
+This document outlines the **proposed** vision and development direction for **Boot.gs**.
+
+> [!IMPORTANT]
+> This roadmap is provided for informational purposes only. It does not represent a binding commitment.
+> Priorities, features, and timelines are subject to change without notice based on community feedback,
+> available resources, and the evolution of the Google Apps Script platform.
+
+## Phase 1: Foundation (Current)
+
+Focus on establishing core stability and essential developer experience.
+
+- [x] Core Dependency Injection (DI) system.
+- [x] Basic HTTP and Trigger decorators.
+- [x] Initial documentation and examples.
+- [x] Automated testing and CI/CD setup.
+
+## Phase 2: Enhanced Features (Planned)
+
+Potential framework expansions to handle more complex scenarios.
+
+- [ ] **Middleware Support**: Research and implementation of request/event interception.
+- [ ] **Validation Decorators**: Exploration of built-in validation support.
+- [ ] **Advanced DI**: Further improvements to the injection system.
+
+## Phase 3: Ecosystem & Tooling (Future Exploration)
+
+Aims to simplify the Boot.gs development lifecycle.
+
+- [ ] **CLI Tool**: Potential scaffolding for new projects.
+- [ ] **Project Templates**: Pre-configured templates for common Apps Script use cases.
+- [ ] **Documentation Improvements**: Enhanced guides and API references.
+- [ ] **Testing Utilities**: Mocks and helpers for testing Apps Script specific code.
+
+## Phase 4: Integration & Optimization (Long-term Vision)
+
+Refinement and integration with wider Google services.
+
+- [ ] **Service Integrations**: Easier interaction with common Google APIs.
+- [ ] **Performance Analysis**: Tools to measure and optimize execution time.
+- [ ] **State Management**: Support for external databases and caching.
+
+---
+
+## Feedback
+
+We value your input! Suggestions and ideas are welcome via [GitHub Issues](https://github.com/bootgs/boot/issues).
+Items marked as "Planned" or "Future Exploration" are not guaranteed to be implemented.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..b27f6fc
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,77 @@
+# Security Policy
+
+This document outlines how to report vulnerabilities in **Boot.gs**. We appreciate the efforts of security researchers
+to help us keep the framework secure.
+
+## Supported Versions
+
+We aim to provide security updates for the following versions of **Boot.gs** on a best-effort basis:
+
+
+
+
+
Version
+
Supported
+
+
+
+
+
>= 1.0.0
+
:white_check_mark:
+
+
+
< 1.0.0
+
:x:
+
+
+
+
+We recommend using the latest stable version to benefit from any security improvements.
+
+## Reporting a Vulnerability
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+If you discover a potential security vulnerability, please report it privately.
+
+1. **Send an Email:** Send a report
+ to [stoianov.maksym+bootgs+security@gmail.com](mailto:stoianov.maksym+bootgs+security@gmail.com).
+2. **Include Details:** To help us understand the issue, please include:
+ - A descriptive title.
+ - Type of issue.
+ - Affected versions.
+ - Step-by-step instructions to reproduce or a proof-of-concept.
+3. **Wait for Response:** We will try to acknowledge your report as soon as possible, depending on our availability.
+
+## Our Security Process
+
+When a vulnerability is reported, we will:
+
+1. **Review:** Evaluate the report to determine if it is a valid security concern.
+2. **Development:** If confirmed, we will work on a fix as our resources and time permit.
+3. **Disclosure:** Once a fix is released, we may provide credit to the reporter (if they wish) and may publish a
+ security advisory.
+
+## Disclosure Policy
+
+- We follow the principle of coordinated vulnerability disclosure.
+- We ask that you give us a reasonable amount of time to resolve the issue before making any information public.
+
+## Recognition
+
+We value the work of security researchers and may credit those who report vulnerabilities in our release notes or
+security advisories at our discretion.
+
+## Security Best Practices for Users
+
+To help keep your applications secure, we recommend following these general best practices:
+
+1. **Keep Dependencies Updated:** Regularly update your project dependencies.
+2. **Secret Management:** Never commit sensitive information (API keys, secrets) to your repository. Use environment
+ variables.
+3. **Principle of Least Privilege:** Grant only necessary permissions in your `appsscript.json`.
+4. **Input Validation:** Always validate and sanitize user input.
+
+---
+
+_Thank you for helping us keep **Boot.gs** secure!_
diff --git a/config/eslint/base.ts b/config/eslint/base.ts
deleted file mode 100644
index f24fe65..0000000
--- a/config/eslint/base.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import js from "@eslint/js";
-
-export default {
- files: ["**/*.{js,mjs,cjs,ts}"],
- plugins: { js },
- extends: ["js/recommended"]
-};
diff --git a/config/eslint/common-ignores.ts b/config/eslint/common-ignores.ts
new file mode 100644
index 0000000..7ff443f
--- /dev/null
+++ b/config/eslint/common-ignores.ts
@@ -0,0 +1,12 @@
+import type { Linter } from "eslint";
+
+/**
+ * Common ESLint ignore paths.
+ *
+ * @see https://eslint.org/docs/latest/use/configure/ignore
+ */
+const config: Linter.Config = {
+ ignores: ["dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js"]
+};
+
+export default config;
diff --git a/config/eslint/custom-rules.ts b/config/eslint/custom-rules.ts
deleted file mode 100644
index ba05161..0000000
--- a/config/eslint/custom-rules.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
- files: ["**/*.{js,mjs,cjs,ts}"],
- rules: {
- "@typescript-eslint/no-unused-vars": "warn",
- "@typescript-eslint/no-explicit-any": "warn",
- "@typescript-eslint/no-empty-object-type": "warn"
- }
-};
diff --git a/config/eslint/env-appsscript.ts b/config/eslint/env-appsscript.ts
new file mode 100644
index 0000000..fca36b9
--- /dev/null
+++ b/config/eslint/env-appsscript.ts
@@ -0,0 +1,44 @@
+import type { Linter } from "eslint";
+
+/**
+ * Google Apps Script environment settings.
+ */
+const config: Linter.Config = {
+ files: ["src/**/*.{js,mjs,cjs,ts,jsx,tsx}"],
+ rules: {
+ "no-restricted-globals": [
+ "error",
+ {
+ name: "setTimeout",
+ message:
+ "Use Utilities.sleep for synchronous pauses. Async timers are not supported in GAS."
+ },
+ { name: "setInterval", message: "Async timers are not supported in GAS." },
+ { name: "clearTimeout", message: "Async timers are not supported in GAS." },
+ { name: "clearInterval", message: "Async timers are not supported in GAS." },
+ { name: "fetch", message: "Use UrlFetchApp.fetch instead." },
+ { name: "atob", message: "Use Utilities.base64Decode instead." },
+ { name: "btoa", message: "Use Utilities.base64Encode instead." },
+ { name: "window", message: "Web APIs are not available in GAS." },
+ { name: "navigator", message: "Web APIs are not available in GAS." },
+ { name: "process", message: "Node.js APIs are not available in GAS." }
+ ],
+ "no-restricted-syntax": [
+ "error",
+ {
+ selector: "CallExpression[callee.name='fetch']",
+ message: "Use UrlFetchApp.fetch instead."
+ },
+ {
+ selector: "NewExpression[callee.name='URL']",
+ message: "URL API is not available in GAS."
+ },
+ {
+ selector: "NewExpression[callee.name='FormData']",
+ message: "FormData API is not available in GAS."
+ }
+ ]
+ }
+};
+
+export default config;
diff --git a/config/eslint/globals.ts b/config/eslint/globals.ts
deleted file mode 100644
index d815fee..0000000
--- a/config/eslint/globals.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import globals from "globals";
-import googleappsscript from "eslint-plugin-googleappsscript";
-
-export default {
- files: ["**/*.{js,mjs,cjs,ts}"],
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.node,
- ...globals.es2020,
- ...googleappsscript.environments.googleappsscript.globals
- }
- }
-};
diff --git a/config/eslint/ignores.ts b/config/eslint/ignores.ts
deleted file mode 100644
index aa76051..0000000
--- a/config/eslint/ignores.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- ignores: ["dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js"]
-};
diff --git a/config/eslint/index.ts b/config/eslint/index.ts
new file mode 100644
index 0000000..fc89271
--- /dev/null
+++ b/config/eslint/index.ts
@@ -0,0 +1,26 @@
+import commonIgnores from "./common-ignores.ts";
+import envAppsscript from "./env-appsscript.ts";
+import langJavascript from "./lang-javascript.ts";
+import langJson from "./lang-json.ts";
+import langMarkdown from "./lang-markdown.ts";
+import langTypescript from "./lang-typescript.ts";
+import overridesTests from "./overrides-tests.ts";
+import rulesJsdoc from "./rules-jsdoc.ts";
+import rulesSpacing from "./rules-spacing.ts";
+import rulesPrettier from "./rules-prettier.ts";
+
+/**
+ * ESLint configurations entry point.
+ */
+export {
+ commonIgnores,
+ envAppsscript,
+ langJavascript,
+ langJson,
+ langMarkdown,
+ langTypescript,
+ rulesJsdoc,
+ rulesSpacing,
+ overridesTests,
+ rulesPrettier
+};
diff --git a/config/eslint/json.ts b/config/eslint/json.ts
deleted file mode 100644
index 9efd993..0000000
--- a/config/eslint/json.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import json from "@eslint/json";
-
-export default [
- {
- files: ["**/*.json"],
- plugins: { json },
- language: "json/json",
- extends: ["json/recommended"]
- },
- {
- files: ["**/*.jsonc"],
- plugins: { json },
- language: "json/jsonc",
- extends: ["json/recommended"]
- },
- {
- files: ["**/*.json5"],
- plugins: { json },
- language: "json/json5",
- extends: ["json/recommended"]
- }
-];
diff --git a/config/eslint/lang-javascript.ts b/config/eslint/lang-javascript.ts
new file mode 100644
index 0000000..2e9ecbc
--- /dev/null
+++ b/config/eslint/lang-javascript.ts
@@ -0,0 +1,14 @@
+import js from "@eslint/js";
+import type { Linter } from "eslint";
+
+/**
+ * JavaScript language settings.
+ *
+ * @see {@link https://eslint.org/docs/latest/rules/ ESLint rules}
+ */
+const config: Linter.Config = {
+ files: ["**/*.{js,mjs,cjs}"],
+ ...js.configs.recommended
+};
+
+export default config;
diff --git a/config/eslint/lang-json.ts b/config/eslint/lang-json.ts
new file mode 100644
index 0000000..7c0b281
--- /dev/null
+++ b/config/eslint/lang-json.ts
@@ -0,0 +1,28 @@
+import json from "@eslint/json";
+import type { Linter } from "eslint";
+
+/**
+ * JSON language settings.
+ *
+ * @see {@link https://www.npmjs.com/package/@eslint/json @eslint/json}
+ */
+const config: Array = [
+ {
+ files: ["**/*.json"],
+ ignores: ["**/tsconfig.json", "**/tsconfig.*.json"],
+ language: "json/json",
+ ...json.configs.recommended
+ },
+ {
+ files: ["**/*.jsonc", "**/tsconfig.json", "**/tsconfig.*.json"],
+ language: "json/jsonc",
+ ...json.configs.recommended
+ },
+ {
+ files: ["**/*.json5"],
+ language: "json/json5",
+ ...json.configs.recommended
+ }
+];
+
+export default config;
diff --git a/config/eslint/lang-markdown.ts b/config/eslint/lang-markdown.ts
new file mode 100644
index 0000000..f561c85
--- /dev/null
+++ b/config/eslint/lang-markdown.ts
@@ -0,0 +1,22 @@
+import markdown from "@eslint/markdown";
+import type { Linter } from "eslint";
+
+/**
+ * Markdown language settings.
+ *
+ * @see {@link https://www.npmjs.com/package/@eslint/markdown @eslint/markdown}
+ */
+const config: Array = [
+ ...markdown.configs.recommended.map((config) => ({
+ ...config,
+ files: ["**/*.md"]
+ })),
+ {
+ files: ["**/*.md"],
+ rules: {
+ "markdown/no-missing-label-refs": "off"
+ }
+ }
+];
+
+export default config;
diff --git a/config/eslint/lang-typescript.ts b/config/eslint/lang-typescript.ts
new file mode 100644
index 0000000..dd80475
--- /dev/null
+++ b/config/eslint/lang-typescript.ts
@@ -0,0 +1,51 @@
+import type { Linter } from "eslint";
+import { parser } from "typescript-eslint";
+import globals from "globals";
+
+/**
+ * TypeScript language settings.
+ *
+ * @see https://typescript-eslint.io/rules/
+ */
+const config: Linter.Config = {
+ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
+ languageOptions: {
+ parser,
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ ...globals.es2020
+ }
+ },
+ rules: {
+ /**
+ * Disallows unused variables.
+ *
+ * @see https://typescript-eslint.io/rules/no-unused-vars/
+ */
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_"
+ }
+ ],
+
+ /**
+ * Disallows usage of the `any` type.
+ *
+ * @see https://typescript-eslint.io/rules/no-explicit-any/
+ */
+ "@typescript-eslint/no-explicit-any": "warn",
+
+ /**
+ * Disallows empty object types.
+ *
+ * @see https://typescript-eslint.io/rules/no-empty-object-type/
+ */
+ "@typescript-eslint/no-empty-object-type": "warn"
+ }
+};
+
+export default config;
diff --git a/config/eslint/markdown.ts b/config/eslint/markdown.ts
deleted file mode 100644
index 93abddb..0000000
--- a/config/eslint/markdown.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import markdown from "@eslint/markdown";
-
-export default {
- files: ["**/*.md"],
- plugins: { markdown },
- language: "markdown/gfm",
- extends: ["markdown/recommended"]
-};
diff --git a/config/eslint/overrides-tests.ts b/config/eslint/overrides-tests.ts
new file mode 100644
index 0000000..abf82cd
--- /dev/null
+++ b/config/eslint/overrides-tests.ts
@@ -0,0 +1,23 @@
+import type { Linter } from "eslint";
+
+/**
+ * Overrides for test files.
+ */
+const config: Linter.Config = {
+ files: ["test/**/*.ts"],
+ rules: {
+ /**
+ * Disallows usage of the `any` type.
+ * @see {@link https://typescript-eslint.io/rules/no-explicit-any/ no-explicit-any}
+ */
+ "@typescript-eslint/no-explicit-any": "error",
+
+ /**
+ * Disallows unused variables.
+ * @see {@link https://typescript-eslint.io/rules/no-unused-vars/ no-unused-vars}
+ */
+ "@typescript-eslint/no-unused-vars": "error"
+ }
+};
+
+export default config;
diff --git a/config/eslint/prettier.ts b/config/eslint/prettier.ts
deleted file mode 100644
index 7397eb2..0000000
--- a/config/eslint/prettier.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { rules } from "eslint-config-prettier";
-
-export default { rules };
diff --git a/config/eslint/rules-jsdoc.ts b/config/eslint/rules-jsdoc.ts
new file mode 100644
index 0000000..d5c3f7b
--- /dev/null
+++ b/config/eslint/rules-jsdoc.ts
@@ -0,0 +1,42 @@
+import type { Linter } from "eslint";
+import jsdoc from "eslint-plugin-jsdoc";
+
+/**
+ * JSDoc comment settings.
+ *
+ * @see {@link https://www.npmjs.com/package/eslint-plugin-jsdoc eslint-plugin-jsdoc}
+ */
+const config: Linter.Config = {
+ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
+ plugins: {
+ jsdoc
+ },
+ rules: {
+ /**
+ * Enforces multiline JSDoc blocks.
+ */
+ "jsdoc/multiline-blocks": [
+ "warn",
+ {
+ noSingleLineBlocks: true
+ }
+ ],
+
+ /**
+ * Enforces a blank line before JSDoc comments.
+ * @see {@link https://eslint.org/docs/latest/rules/lines-around-comment lines-around-comment}
+ */
+ "lines-around-comment": [
+ "warn",
+ {
+ beforeBlockComment: true,
+ allowBlockStart: true,
+ allowObjectStart: true,
+ allowArrayStart: true,
+ allowClassStart: true
+ }
+ ]
+ }
+};
+
+export default config;
diff --git a/config/eslint/rules-prettier.ts b/config/eslint/rules-prettier.ts
new file mode 100644
index 0000000..0cbb3d8
--- /dev/null
+++ b/config/eslint/rules-prettier.ts
@@ -0,0 +1,12 @@
+import { rules } from "eslint-config-prettier";
+import type { Linter } from "eslint";
+
+/**
+ * Prettier integration rules.
+ * Disables ESLint rules that might conflict with Prettier.
+ *
+ * @see {@link https://github.com/prettier/eslint-config-prettier eslint-config-prettier configuration}
+ */
+const config: Linter.Config = { rules };
+
+export default config;
diff --git a/config/eslint/rules-spacing.ts b/config/eslint/rules-spacing.ts
new file mode 100644
index 0000000..5815886
--- /dev/null
+++ b/config/eslint/rules-spacing.ts
@@ -0,0 +1,49 @@
+import type { Linter } from "eslint";
+
+/**
+ * Rules for managing spacing within brackets.
+ */
+const config: Linter.Config = {
+ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
+ rules: {
+ /**
+ * Requires spacing inside curly braces.
+ * Aligned with Prettier (bracketSpacing: true).
+ * @see {@link https://eslint.org/docs/latest/rules/object-curly-spacing object-curly-spacing}
+ */
+ "object-curly-spacing": ["warn", "always"],
+
+ /**
+ * Requires spacing inside square brackets.
+ * Note: Prettier does not support this and always removes spacing for arrays.
+ * @see {@link https://eslint.org/docs/latest/rules/array-bracket-spacing array-bracket-spacing}
+ */
+ "array-bracket-spacing": ["warn", "always"],
+
+ /**
+ * Requires spacing inside computed properties.
+ * @see {@link https://eslint.org/docs/latest/rules/computed-property-spacing computed-property-spacing}
+ */
+ "computed-property-spacing": ["warn", "always"],
+
+ /**
+ * Requires a blank line between constants and blocks.
+ * @see {@link https://eslint.org/docs/latest/rules/padding-line-between-statements padding-line-between-statements}
+ */
+ "padding-line-between-statements": [
+ "warn",
+ {
+ blankLine: "always",
+ prev: ["const", "let", "var"],
+ next: ["if", "for", "while", "switch", "try", "do", "block", "block-like"]
+ },
+ {
+ blankLine: "always",
+ prev: ["if", "for", "while", "switch", "try", "do", "block", "block-like"],
+ next: ["const", "let", "var"]
+ }
+ ]
+ }
+};
+
+export default config;
diff --git a/config/types/eslint-plugin-googleappsscript.d.ts b/config/types/eslint-plugin-jsdoc.d.ts
similarity index 56%
rename from config/types/eslint-plugin-googleappsscript.d.ts
rename to config/types/eslint-plugin-jsdoc.d.ts
index c7db861..f80fcb8 100644
--- a/config/types/eslint-plugin-googleappsscript.d.ts
+++ b/config/types/eslint-plugin-jsdoc.d.ts
@@ -1,6 +1,6 @@
-declare module "eslint-plugin-googleappsscript" {
+declare module "eslint-plugin-jsdoc" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const plugin: any;
- export = plugin;
+ export default plugin;
}
diff --git a/config/typescript/tsconfig.appsscript.json b/config/typescript/tsconfig.appsscript.json
new file mode 100644
index 0000000..2f50ad9
--- /dev/null
+++ b/config/typescript/tsconfig.appsscript.json
@@ -0,0 +1,109 @@
+{
+ /**
+ * Instructs the TypeScript compiler how to compile the .ts files.
+ * @see https://www.typescriptlang.org/tsconfig#compilerOptions
+ */
+ "compilerOptions": {
+ /**
+ * Allow importing files with a TypeScript-specific extension (.ts, .tsx, etc.).
+ * @see https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions
+ */
+ "allowImportingTsExtensions": false,
+
+ /**
+ * Disable error reporting for unreachable code.
+ * @see https://www.typescriptlang.org/tsconfig#allowUnreachableCode
+ */
+ "allowUnreachableCode": false,
+
+ /**
+ * Disable error reporting for unused labels.
+ * @see https://www.typescriptlang.org/tsconfig#allowUnusedLabels
+ */
+ "allowUnusedLabels": false,
+
+ /**
+ * Enable project compilation.
+ * @see https://www.typescriptlang.org/tsconfig#composite
+ */
+ "composite": true,
+
+ /**
+ * Generate .d.ts files from TypeScript and JavaScript files in your project.
+ * @see https://www.typescriptlang.org/tsconfig#declaration
+ */
+ "declaration": true,
+
+ /**
+ * Ensure each file can be safely transpiled without relying on other imports.
+ * @see https://www.typescriptlang.org/tsconfig#isolatedModules
+ */
+ "isolatedModules": false,
+
+ /**
+ * List of library files to be included in the compilation.
+ * @see https://www.typescriptlang.org/tsconfig#lib
+ */
+ "lib": ["ESNext"],
+
+ /**
+ * Disable emitting files from a compilation.
+ * @see https://www.typescriptlang.org/tsconfig#noEmit
+ */
+ "noEmit": false,
+
+ /**
+ * Specify an output folder for all emitted files.
+ * @see https://www.typescriptlang.org/tsconfig#outDir
+ */
+ "outDir": "../../dist",
+
+ /**
+ * Specify the root folder within your source files.
+ * @see https://www.typescriptlang.org/tsconfig#rootDir
+ */
+ "rootDir": "../../src",
+
+ /**
+ * Specify the file to store incremental compilation information.
+ * @see https://www.typescriptlang.org/tsconfig#tsBuildInfoFile
+ */
+ "tsBuildInfoFile": "../../node_modules/.tmp/tsconfig.appsscript.tsbuildinfo",
+
+ /**
+ * List of folders to include type definitions from.
+ * @see https://www.typescriptlang.org/tsconfig#typeRoots
+ */
+ "typeRoots": ["../../node_modules/@types", "../../node_modules", "../types"],
+
+ /**
+ * Specify type package names to be included without being referenced in a source file.
+ * @see https://www.typescriptlang.org/tsconfig#types
+ */
+ "types": ["google-apps-script", "reflect-metadata"],
+
+ /**
+ * Emit ECMAScript-standard-compliant class fields.
+ * @see https://www.typescriptlang.org/tsconfig#useDefineForClassFields
+ */
+ "useDefineForClassFields": false
+ },
+
+ /**
+ * Specifies a list of files to be excluded from compilation.
+ * @see https://www.typescriptlang.org/tsconfig#exclude
+ */
+ "exclude": ["../../dist", "../../node_modules"],
+
+ /**
+ * Path to a base configuration file to inherit from.
+ * @see https://www.typescriptlang.org/tsconfig#extends
+ */
+ "extends": "./tsconfig.base.json",
+
+ /**
+ * Specifies a list of glob patterns that match files to be included in compilation.
+ * @see https://www.typescriptlang.org/tsconfig#include
+ */
+ "include": ["../../src/**/*"]
+}
diff --git a/config/typescript/tsconfig.base.json b/config/typescript/tsconfig.base.json
new file mode 100644
index 0000000..c875227
--- /dev/null
+++ b/config/typescript/tsconfig.base.json
@@ -0,0 +1,123 @@
+{
+ /**
+ * Instructs the TypeScript compiler how to compile the .ts files.
+ * @see https://www.typescriptlang.org/tsconfig#compilerOptions
+ */
+ "compilerOptions": {
+ /**
+ * Allow importing files with a TypeScript-specific extension (.ts, .tsx, etc.).
+ * @see https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions
+ */
+ "allowImportingTsExtensions": true,
+
+ /**
+ * Base directory to resolve non-relative module names.
+ * @see https://www.typescriptlang.org/tsconfig#baseUrl
+ */
+ "baseUrl": "../../src",
+
+ /**
+ * Emit design-type metadata for decorated declarations in source files.
+ * @see https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata
+ */
+ "emitDecoratorMetadata": true,
+
+ /**
+ * Emit additional JavaScript to ease support for importing CommonJS modules.
+ * @see https://www.typescriptlang.org/tsconfig#esModuleInterop
+ */
+ "esModuleInterop": true,
+
+ /**
+ * Enable experimental support for legacy experimental decorators.
+ * @see https://www.typescriptlang.org/tsconfig#experimentalDecorators
+ */
+ "experimentalDecorators": true,
+
+ /**
+ * Ensure that the casing is correct in imports.
+ * @see https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames
+ */
+ "forceConsistentCasingInFileNames": true,
+
+ /**
+ * Specify what module code is generated.
+ * @see https://www.typescriptlang.org/tsconfig#module
+ */
+ "module": "ESNext",
+
+ /**
+ * Control what method is used to detect whether a file is a script or a module.
+ * @see https://www.typescriptlang.org/tsconfig#moduleDetection
+ */
+ "moduleDetection": "force",
+
+ /**
+ * Specify how TypeScript looks up a file from a given module specifier.
+ * @see https://www.typescriptlang.org/tsconfig#moduleResolution
+ */
+ "moduleResolution": "bundler",
+
+ /**
+ * Disable emitting files from a compilation.
+ * @see https://www.typescriptlang.org/tsconfig#noEmit
+ */
+ "noEmit": true,
+
+ /**
+ * Enable error reporting for fallthrough cases in switch statements.
+ * @see https://www.typescriptlang.org/tsconfig#noFallthroughCasesInSwitch
+ */
+ "noFallthroughCasesInSwitch": true,
+
+ /**
+ * Ensure that any non-relative imports are not only present but also have a valid type definition.
+ * @see https://www.typescriptlang.org/tsconfig#noUncheckedSideEffectImports
+ */
+ "noUncheckedSideEffectImports": true,
+
+ /**
+ * Enable error reporting when local variables aren't read.
+ * @see https://www.typescriptlang.org/tsconfig#noUnusedLocals
+ */
+ "noUnusedLocals": false,
+
+ /**
+ * Raise an error when a function parameter isn't read.
+ * @see https://www.typescriptlang.org/tsconfig#noUnusedParameters
+ */
+ "noUnusedParameters": false,
+
+ /**
+ * A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
+ * @see https://www.typescriptlang.org/tsconfig#paths
+ */
+ "paths": {
+ "src/*": ["./*"]
+ },
+
+ /**
+ * Specify the root folder within your source files.
+ * @see https://www.typescriptlang.org/tsconfig#rootDir
+ */
+ "rootDir": "../../",
+
+ /**
+ * Skip type checking all .d.ts files.
+ * @see https://www.typescriptlang.org/tsconfig#skipLibCheck
+ */
+ "skipLibCheck": true,
+
+ /**
+ * Enable all strict type-checking options.
+ * @see https://www.typescriptlang.org/tsconfig#strict
+ */
+ "strict": true,
+
+ /**
+ * Set the JavaScript language version for emitted JavaScript and include compatible library declarations.
+ * @see https://www.typescriptlang.org/tsconfig#target
+ */
+ "target": "ESNext"
+ }
+}
diff --git a/config/typescript/tsconfig.json b/config/typescript/tsconfig.json
deleted file mode 100644
index 0f99155..0000000
--- a/config/typescript/tsconfig.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "compilerOptions": {
- "rootDir": "../..",
- "typeRoots": ["../../node_modules/@types", "../types"],
- "types": ["google-apps-script"],
- "tsBuildInfoFile": "../../node_modules/.tmp/tsconfig.appsscript.tsbuildinfo",
- "target": "ESNext",
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true,
- "useDefineForClassFields": false,
- "lib": ["ESNext"],
- "module": "ESNext",
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
- "allowImportingTsExtensions": false,
- "isolatedModules": false,
- "moduleDetection": "force",
- "noEmit": true,
- "strict": true,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "composite": false,
- "baseUrl": "../..",
- "paths": {
- "src/*": ["../../src/*"]
- }
- },
- "include": ["../../src/**/*", "../../test/**/*"],
- "exclude": ["../../node_modules", "../../dist"]
-}
diff --git a/config/typescript/tsconfig.node.json b/config/typescript/tsconfig.node.json
index cd8da03..53d6524 100644
--- a/config/typescript/tsconfig.node.json
+++ b/config/typescript/tsconfig.node.json
@@ -1,29 +1,61 @@
{
+ /**
+ * Instructs the TypeScript compiler how to compile the .ts files.
+ * @see https://www.typescriptlang.org/tsconfig#compilerOptions
+ */
"compilerOptions": {
- "rootDir": "../..",
- "typeRoots": ["../../node_modules/@types", "../types"],
- "tsBuildInfoFile": "../../node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2023",
- "lib": ["ES2023", "dom"],
- "module": "ESNext",
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
- "allowImportingTsExtensions": true,
+ /**
+ * Ensure each file can be safely transpiled without relying on other imports.
+ * @see https://www.typescriptlang.org/tsconfig#isolatedModules
+ */
"isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "strict": true,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
- "baseUrl": "../..",
- "paths": {
- "src/*": ["../../src/*"]
- }
+
+ /**
+ * List of library files to be included in the compilation.
+ * @see https://www.typescriptlang.org/tsconfig#lib
+ */
+ "lib": ["ES2023", "dom"],
+
+ /**
+ * Set the JavaScript language version for emitted JavaScript and include compatible library declarations.
+ * @see https://www.typescriptlang.org/tsconfig#target
+ */
+ "target": "ES2023",
+
+ /**
+ * Specify the file to store incremental compilation information.
+ * @see https://www.typescriptlang.org/tsconfig#tsBuildInfoFile
+ */
+ "tsBuildInfoFile": "../../node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ /**
+ * List of folders to include type definitions from.
+ * @see https://www.typescriptlang.org/tsconfig#typeRoots
+ */
+ "typeRoots": ["../../node_modules/@types", "../types"]
},
- "include": ["../../config/**/*", "../../vite.config.ts"],
- "exclude": ["../../node_modules", "../../dist"]
+
+ /**
+ * Specifies a list of files to be excluded from compilation.
+ * @see https://www.typescriptlang.org/tsconfig#exclude
+ */
+ "exclude": ["../../dist", "../../node_modules"],
+
+ /**
+ * Path to a base configuration file to inherit from.
+ * @see https://www.typescriptlang.org/tsconfig#extends
+ */
+ "extends": "./tsconfig.base.json",
+
+ /**
+ * Specifies a list of glob patterns that match files to be included in compilation.
+ * @see https://www.typescriptlang.org/tsconfig#include
+ */
+ "include": [
+ "../../config/**/*",
+ "../../eslint.config.js",
+ "../../src/**/*",
+ "../../test/**/*",
+ "../../vitest.config.ts"
+ ]
}
diff --git a/config/vite/buildAliases.ts b/config/vite/buildAliases.ts
deleted file mode 100644
index 0592689..0000000
--- a/config/vite/buildAliases.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { existsSync, readdirSync } from "node:fs";
-import { join } from "path";
-import { AliasOptions } from "vite";
-
-/**
- * Builds a Vite alias object based on the subdirectories of a given root directory.
- *
- * This function reads the contents of the provided `rootDir` and returns
- * an object where the keys are the names of all visible subdirectories,
- * and the values are their absolute paths.
- * Useful for setting up `resolve.alias` in Vite.
- *
- * @param rootDir - The absolute url to the directory to scan for subfolders.
- * @returns An alias configuration object for use in Vite.
- */
-export function buildAliases(rootDir: string): AliasOptions {
- return existsSync(rootDir)
- ? Object.fromEntries(
- readdirSync(rootDir, { withFileTypes: true })
- .filter(
- dirent => dirent.isDirectory() && !dirent.name.startsWith(".")
- )
- .map(dirent => [dirent.name, join(rootDir, dirent.name)])
- )
- : {};
-}
diff --git a/config/vite/buildResolversConfig.ts b/config/vite/buildResolversConfig.ts
deleted file mode 100644
index a4e5563..0000000
--- a/config/vite/buildResolversConfig.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { BuildOptions, ResolveOptions } from "./types";
-
-/**
- * Generates the Vite resolve configuration with aliases based on the target environment (`Apps Script` or `WebApp`).
- *
- * @param options - Build options including paths and target type.
- * @returns Resolve redux with generated aliases.
- */
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export function buildResolversConfig(options: BuildOptions): ResolveOptions {
- return {
- alias: {}
- };
-}
diff --git a/config/vite/factories/index.ts b/config/vite/factories/index.ts
new file mode 100644
index 0000000..fa5d391
--- /dev/null
+++ b/config/vite/factories/index.ts
@@ -0,0 +1,2 @@
+export * from "./resolveConfig";
+export * from "./testConfig";
diff --git a/config/vite/factories/resolveConfig.ts b/config/vite/factories/resolveConfig.ts
new file mode 100644
index 0000000..985cdab
--- /dev/null
+++ b/config/vite/factories/resolveConfig.ts
@@ -0,0 +1,21 @@
+import { BuildOptions, ResolveOptions } from "../types";
+import { buildAliases } from "../utils";
+
+/**
+ * Generates Vite resolve configuration.
+ *
+ * @param {BuildOptions} options - Build configuration options.
+ * @returns {ResolveOptions} Vite resolve configuration object.
+ * @see https://vite.dev/config/shared-options.html#resolve
+ */
+export function resolveConfig({ paths }: BuildOptions): ResolveOptions {
+ return {
+ /**
+ * @see https://vite.dev/config/shared-options.html#resolve-alias
+ */
+ alias: {
+ src: paths.src,
+ ...buildAliases(paths.src)
+ }
+ };
+}
diff --git a/config/vite/factories/testConfig.ts b/config/vite/factories/testConfig.ts
new file mode 100644
index 0000000..2950f94
--- /dev/null
+++ b/config/vite/factories/testConfig.ts
@@ -0,0 +1,15 @@
+import { type ViteUserConfig } from "vitest/config";
+import { BuildOptions } from "../types";
+
+/**
+ * Generates Vitest configuration.
+ *
+ * @param {BuildOptions} _options - Build configuration options.
+ * @returns {} Vitest configuration object.
+ * @see https://vitest.dev/config/
+ */
+export const testConfig = (_options: BuildOptions): ViteUserConfig["test"] => {
+ return {
+ globals: true
+ };
+};
diff --git a/config/vite/index.ts b/config/vite/index.ts
index 683f604..ae9815b 100644
--- a/config/vite/index.ts
+++ b/config/vite/index.ts
@@ -1,4 +1,3 @@
export * from "./types";
-
-export { buildAliases } from "./buildAliases";
-export { buildResolversConfig } from "./buildResolversConfig";
+export * from "./factories";
+export * from "./utils";
diff --git a/config/vite/types/BuildOptions.ts b/config/vite/types/BuildOptions.ts
index 3b7053d..5ae673a 100644
--- a/config/vite/types/BuildOptions.ts
+++ b/config/vite/types/BuildOptions.ts
@@ -1,19 +1,28 @@
import { ConfigEnv } from "vite";
-import type { BuildPaths } from "./BuildPaths";
+import { BuildPaths } from "./BuildPaths";
/**
- * Represents the configuration options for the build process in Vite.
- * Extends Vite's `ConfigEnv` to include project-specific paths, environment flags,
- * and target type (`Apps Script` or `WebApp`) for customization during the build.
+ * Build configuration options.
+ * Extends Vite's `ConfigEnv` with project-specific paths and flags.
*/
export interface BuildOptions extends ConfigEnv {
/**
- * Paths used in the project, including source and distribution directories.
+ * Project paths.
*/
paths: BuildPaths;
/**
- * Indicates if the build is in development mode.
+ * Google Apps Script target flag.
+ */
+ isAppsScript?: boolean;
+
+ /**
+ * Web App target flag.
+ */
+ isWebApp?: boolean;
+
+ /**
+ * Development mode flag.
*/
isDev: boolean;
}
diff --git a/config/vite/types/BuildPaths.ts b/config/vite/types/BuildPaths.ts
index 2d36420..f122fca 100644
--- a/config/vite/types/BuildPaths.ts
+++ b/config/vite/types/BuildPaths.ts
@@ -1,30 +1,29 @@
/**
- * Represents the directory paths used in the build process.
- * Provides the paths for the root directory, source files, distribution, and optionally configuration and public directories.
+ * Directory paths used in the build process.
*/
export type BuildPaths = {
/**
- * The root directory of the project.
+ * Project root directory.
*/
root: string;
/**
- * Optional url to the configuration directory.
+ * Configuration directory.
*/
config: string;
/**
- * The output directory for the build.
+ * Build output directory.
*/
dist: string;
/**
- * Optional url to the public directory for static assets.
+ * Public directory for static assets.
*/
public?: string;
/**
- * The source directory for the project files.
+ * Source directory.
*/
src: string;
};
diff --git a/config/vite/types/ResolveOptions.ts b/config/vite/types/ResolveOptions.ts
index f0a00a6..569ba7b 100644
--- a/config/vite/types/ResolveOptions.ts
+++ b/config/vite/types/ResolveOptions.ts
@@ -1,12 +1,11 @@
import { AliasOptions, ResolveOptions as ViteResolveOptions } from "vite";
/**
- * Represents Vite's resolve configuration options, with the addition of optional alias configuration.
- * This type is used to define how Vite should resolve modules, including custom aliases.
+ * Vite resolve options with optional aliases.
*/
export type ResolveOptions = ViteResolveOptions & {
/**
- * Optional alias configuration to create custom module aliases.
+ * Module aliases.
*/
alias?: AliasOptions;
};
diff --git a/config/vite/types/index.ts b/config/vite/types/index.ts
index 1368986..68be9ca 100644
--- a/config/vite/types/index.ts
+++ b/config/vite/types/index.ts
@@ -1,3 +1,3 @@
-export type { BuildOptions } from "./BuildOptions";
-export type { BuildPaths } from "./BuildPaths";
-export type { ResolveOptions } from "./ResolveOptions";
+export * from "./BuildOptions";
+export * from "./BuildPaths";
+export * from "./ResolveOptions";
diff --git a/config/vite/utils/buildAliases.ts b/config/vite/utils/buildAliases.ts
new file mode 100644
index 0000000..2127757
--- /dev/null
+++ b/config/vite/utils/buildAliases.ts
@@ -0,0 +1,23 @@
+import { existsSync, readdirSync } from "node:fs";
+import { join } from "node:path";
+import { AliasOptions } from "vite";
+
+/**
+ * Builds Vite aliases based on subdirectories of the given root.
+ *
+ * @param {string} rootDir - Absolute path to the directory to scan.
+ * @returns {AliasOptions} Alias configuration object.
+ * @see https://vite.dev/config/shared-options.html#resolve-alias
+ */
+export function buildAliases(rootDir: string): AliasOptions {
+ return existsSync(rootDir)
+ ? Object.fromEntries(
+ readdirSync(rootDir, { withFileTypes: true })
+ .filter((dirent) => !dirent.name.startsWith("."))
+ .map((dirent) => {
+ const name = dirent.name.replace(/\.ts$/, "");
+ return [name, join(rootDir, dirent.name)];
+ })
+ )
+ : {};
+}
diff --git a/config/vite/utils/getAppVersion.ts b/config/vite/utils/getAppVersion.ts
new file mode 100644
index 0000000..a37a1bf
--- /dev/null
+++ b/config/vite/utils/getAppVersion.ts
@@ -0,0 +1,19 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+/**
+ * Retrieves the application version from `package.json`.
+ *
+ * @param {string} rootDir - Root directory of the project.
+ * @returns {string | null} Version string or `null`.
+ */
+export const getAppVersion = (rootDir: string = process.cwd()): string | null => {
+ try {
+ const packageJsonPath = resolve(rootDir, "package.json");
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
+
+ return packageJson.version || null;
+ } catch {
+ return null;
+ }
+};
diff --git a/config/vite/utils/getBuildOptions.ts b/config/vite/utils/getBuildOptions.ts
new file mode 100644
index 0000000..357bfbc
--- /dev/null
+++ b/config/vite/utils/getBuildOptions.ts
@@ -0,0 +1,32 @@
+import { join, resolve } from "node:path";
+import { type ConfigEnv } from "vite";
+import { type BuildOptions, type BuildPaths } from "../types";
+
+/**
+ * Generates build options based on the configuration environment.
+ *
+ * @param {ConfigEnv} env - Vite configuration environment.
+ * @returns {BuildOptions} Project build options.
+ */
+export const getBuildOptions = (env: ConfigEnv): BuildOptions => {
+ const { mode } = env;
+ const rootDir = process.cwd();
+
+ const paths: BuildPaths = {
+ root: rootDir,
+ config: join(rootDir, "config"),
+ dist: resolve(rootDir, "dist"),
+ src: join(rootDir, "src")
+ };
+
+ const isAppsScript = process.argv.includes("--target=appsscript");
+ const isWebApp = process.argv.includes("--target=webapp");
+
+ return {
+ ...env,
+ paths,
+ isAppsScript,
+ isWebApp,
+ isDev: mode === "development"
+ };
+};
diff --git a/config/vite/utils/index.ts b/config/vite/utils/index.ts
new file mode 100644
index 0000000..55d0a8e
--- /dev/null
+++ b/config/vite/utils/index.ts
@@ -0,0 +1,3 @@
+export * from "./buildAliases";
+export * from "./getAppVersion";
+export * from "./getBuildOptions";
diff --git a/eslint.config.js b/eslint.config.js
index 5c35021..a8e5d22 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,20 +1,29 @@
import tsEslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
-import base from "./config/eslint/base.ts";
-import globals from "./config/eslint/globals.ts";
-import prettier from "./config/eslint/prettier.ts";
-import json from "./config/eslint/json.ts";
-import markdown from "./config/eslint/markdown.ts";
-import customRules from "./config/eslint/custom-rules.ts";
-import ignores from "./config/eslint/ignores.ts";
+import {
+ commonIgnores,
+ envAppsscript,
+ langJavascript,
+ langJson,
+ langMarkdown,
+ langTypescript,
+ overridesTests,
+ rulesJsdoc,
+ rulesPrettier,
+ rulesSpacing
+} from "./config/eslint/index.ts";
export default defineConfig([
- base,
- globals,
+ langJavascript,
tsEslint.configs.recommended,
- prettier,
- ...json,
- markdown,
- customRules,
- ignores
+ langTypescript,
+ envAppsscript,
+ rulesPrettier,
+ ...langJson,
+ ...langMarkdown,
+ rulesPrettier,
+ rulesJsdoc,
+ rulesSpacing,
+ overridesTests,
+ commonIgnores
]);
diff --git a/package-lock.json b/package-lock.json
index 6b0eb71..5ec751a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,24 +23,128 @@
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
- "@eslint/json": "^0.13.2",
- "@eslint/markdown": "^7.5.0",
- "@types/google-apps-script": "^2.0.7",
- "@types/node": "^24.9.1",
- "eslint": "^9.38.0",
+ "@eslint/json": "^0.14.0",
+ "@eslint/markdown": "^7.5.1",
+ "@types/google-apps-script": "^2.0.8",
+ "@types/node": "^25.0.9",
+ "@vitest/coverage-v8": "^4.0.18",
+ "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-googleappsscript": "^1.0.5",
+ "eslint-plugin-jsdoc": "^62.5.2",
"husky": "^9.1.7",
- "prettier": "^3.6.2",
- "typescript": "^5.9.3",
- "typescript-eslint": "^8.46.2",
- "vitest": "^4.0.4"
+ "prettier": "^3.8.0",
+ "typescript-eslint": "^8.53.0",
+ "vitest": "^4.0.17"
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.3"
+ }
+ },
+ "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.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "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/@es-joy/jsdoccomment": {
+ "version": "0.84.0",
+ "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.84.0.tgz",
+ "integrity": "sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.8",
+ "@typescript-eslint/types": "^8.54.0",
+ "comment-parser": "1.4.5",
+ "esquery": "^1.7.0",
+ "jsdoc-type-pratt-parser": "~7.1.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
+ "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
+ "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/@es-joy/resolve.exports": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz",
+ "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
- "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
@@ -55,9 +159,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
- "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
@@ -72,9 +176,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
- "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
@@ -89,9 +193,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
- "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
@@ -106,9 +210,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
- "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
@@ -123,9 +227,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
- "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
@@ -140,9 +244,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
- "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
@@ -157,9 +261,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
- "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
@@ -174,9 +278,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
- "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
@@ -191,9 +295,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
- "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
@@ -208,9 +312,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
- "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
@@ -225,9 +329,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
- "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
@@ -242,9 +346,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
- "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
@@ -259,9 +363,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
- "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
@@ -276,9 +380,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
- "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
@@ -293,9 +397,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
- "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
@@ -310,9 +414,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
- "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
@@ -327,9 +431,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
- "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
@@ -344,9 +448,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
- "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
@@ -361,9 +465,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
- "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
@@ -378,9 +482,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
- "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
@@ -395,9 +499,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
- "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
@@ -412,9 +516,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
- "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
@@ -429,9 +533,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
- "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
@@ -446,9 +550,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
- "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
@@ -463,9 +567,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
- "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
@@ -480,9 +584,9 @@
}
},
"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==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -512,9 +616,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "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": {
@@ -537,35 +641,22 @@
}
},
"node_modules/@eslint/config-helpers": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
- "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
+ "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.16.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers/node_modules/@eslint/core": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
- "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
+ "@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
- "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "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": {
@@ -600,9 +691,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.38.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
- "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -613,15 +704,15 @@
}
},
"node_modules/@eslint/json": {
- "version": "0.13.2",
- "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.13.2.tgz",
- "integrity": "sha512-yWLyRE18rHgHXhWigRpiyv1LDPkvWtC6oa7QHXW7YdP6gosJoq7BiLZW2yCs9U7zN7X4U3ZeOJjepA10XAOIMw==",
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.14.0.tgz",
+ "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.15.2",
- "@eslint/plugin-kit": "^0.3.5",
- "@humanwhocodes/momoa": "^3.3.9",
+ "@eslint/core": "^0.17.0",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanwhocodes/momoa": "^3.3.10",
"natural-compare": "^1.4.0"
},
"engines": {
@@ -629,17 +720,17 @@
}
},
"node_modules/@eslint/markdown": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/@eslint/markdown/-/markdown-7.5.0.tgz",
- "integrity": "sha512-reKloVSpytg4ene3yviPJcUO7zglpNn9kWNRiSQ/8gBbBFMKW5Q042LaCi3wv2vVtbPNnLrl6WvhRAHeBd43QA==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint/markdown/-/markdown-7.5.1.tgz",
+ "integrity": "sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==",
"dev": true,
"license": "MIT",
"workspaces": [
"examples/*"
],
"dependencies": {
- "@eslint/core": "^0.16.0",
- "@eslint/plugin-kit": "^0.4.0",
+ "@eslint/core": "^0.17.0",
+ "@eslint/plugin-kit": "^0.4.1",
"github-slugger": "^2.0.0",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
@@ -652,33 +743,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@eslint/markdown/node_modules/@eslint/core": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
- "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
- "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/markdown/node_modules/@eslint/plugin-kit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
- "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.16.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/@eslint/object-schema": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
@@ -690,13 +754,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
- "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "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.15.2",
+ "@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
@@ -756,9 +820,9 @@
}
},
"node_modules/@humanwhocodes/momoa": {
- "version": "3.3.9",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.9.tgz",
- "integrity": "sha512-LHw6Op4bJb3/3KZgOgwflJx5zY9XOy0NU1NuyUFKGdTwHYmP+PbnQGCYQJ8NVNlulLfQish34b0VuUlLYP3AXA==",
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz",
+ "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -779,55 +843,38 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "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/@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==",
+ "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",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
"engines": {
- "node": ">= 8"
+ "node": ">=6.0.0"
}
},
- "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==",
+ "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",
- "engines": {
- "node": ">= 8"
- }
+ "license": "MIT"
},
- "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==",
+ "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": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
- "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -839,9 +886,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
- "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -853,9 +900,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
- "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -867,9 +914,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
- "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -881,9 +928,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
- "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -895,9 +942,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
- "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -909,9 +956,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
- "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -923,9 +970,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
- "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -937,9 +984,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
- "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -951,9 +998,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
- "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -965,9 +1012,23 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
- "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -979,9 +1040,23 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
- "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -993,9 +1068,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
- "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -1007,9 +1082,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
- "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -1021,9 +1096,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
- "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -1035,9 +1110,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
- "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -1049,9 +1124,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
- "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -1062,10 +1137,24 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
- "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -1077,9 +1166,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
- "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -1091,9 +1180,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
- "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -1105,9 +1194,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
- "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -1119,9 +1208,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
- "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1132,10 +1221,23 @@
"win32"
]
},
- "node_modules/@standard-schema/spec": {
+ "node_modules/@sindresorhus/base62": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
- "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz",
+ "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
@@ -1175,9 +1277,9 @@
"license": "MIT"
},
"node_modules/@types/google-apps-script": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/google-apps-script/-/google-apps-script-2.0.7.tgz",
- "integrity": "sha512-Yd5ham8GJNPoKSMt9Ep+B5IJzmfsvSNkOF1DWGQDyz66+1O/Jw/TEe6fltJcTKRvNlCCH5ZaGmmYDOgadA8QYQ==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@types/google-apps-script/-/google-apps-script-2.0.8.tgz",
+ "integrity": "sha512-mGPmzzdgBu1DlwrjOhFQ8u0se6AF/z4OpaTzOGwNKxwXZjE7J+IssfE3oL24j/S/p6aFiSTIptHZvbyqeZD8vA==",
"dev": true,
"license": "MIT"
},
@@ -1206,12 +1308,11 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
- "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
+ "version": "25.0.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
+ "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1224,21 +1325,20 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
- "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
+ "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.46.2",
- "@typescript-eslint/type-utils": "8.46.2",
- "@typescript-eslint/utils": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2",
- "graphemer": "^1.4.0",
- "ignore": "^7.0.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/type-utils": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1248,7 +1348,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.46.2",
+ "@typescript-eslint/parser": "^8.53.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -1264,18 +1364,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
- "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
+ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1290,15 +1389,15 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
- "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
+ "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.46.2",
- "@typescript-eslint/types": "^8.46.2",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.53.0",
+ "@typescript-eslint/types": "^8.53.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1312,14 +1411,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
- "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
+ "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2"
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1330,9 +1429,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
- "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
+ "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1347,17 +1446,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
- "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
+ "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2",
- "@typescript-eslint/utils": "8.46.2",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1372,9 +1471,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
- "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
+ "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1386,22 +1485,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
- "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
+ "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.46.2",
- "@typescript-eslint/tsconfig-utils": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2",
- "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"
+ "@typescript-eslint/project-service": "8.53.0",
+ "@typescript-eslint/tsconfig-utils": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1425,13 +1523,13 @@
}
},
"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==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -1441,16 +1539,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
- "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
+ "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1465,13 +1563,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
- "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
+ "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/types": "8.53.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -1482,18 +1580,49 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
+ "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.0.18",
+ "ast-v8-to-istanbul": "^0.3.10",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "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.18",
+ "vitest": "4.0.18"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@vitest/expect": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.4.tgz",
- "integrity": "sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.0.4",
- "@vitest/utils": "4.0.4",
- "chai": "^6.0.1",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
},
"funding": {
@@ -1501,15 +1630,15 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.4.tgz",
- "integrity": "sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.0.4",
+ "@vitest/spy": "4.0.18",
"estree-walker": "^3.0.3",
- "magic-string": "^0.30.19"
+ "magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -1528,9 +1657,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.4.tgz",
- "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1541,13 +1670,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.4.tgz",
- "integrity": "sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.0.4",
+ "@vitest/utils": "4.0.18",
"pathe": "^2.0.3"
},
"funding": {
@@ -1555,14 +1684,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.4.tgz",
- "integrity": "sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
+ "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.4",
- "magic-string": "^0.30.19",
+ "@vitest/pretty-format": "4.0.18",
+ "magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
@@ -1570,9 +1699,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.4.tgz",
- "integrity": "sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
+ "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1580,13 +1709,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.4.tgz",
- "integrity": "sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
+ "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.4",
+ "@vitest/pretty-format": "4.0.18",
"tinyrainbow": "^3.0.3"
},
"funding": {
@@ -1599,7 +1728,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1618,9 +1746,9 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1669,6 +1797,16 @@
"reflect-metadata": "^0.2.2"
}
},
+ "node_modules/are-docs-informative": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
+ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1686,6 +1824,18 @@
"node": ">=12"
}
},
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz",
+ "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1704,19 +1854,6 @@
"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/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1739,9 +1876,9 @@
}
},
"node_modules/chai": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz",
- "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1796,6 +1933,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/comment-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.5.tgz",
+ "integrity": "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1889,9 +2036,9 @@
"license": "MIT"
},
"node_modules/esbuild": {
- "version": "0.25.11",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
- "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1902,32 +2049,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.11",
- "@esbuild/android-arm": "0.25.11",
- "@esbuild/android-arm64": "0.25.11",
- "@esbuild/android-x64": "0.25.11",
- "@esbuild/darwin-arm64": "0.25.11",
- "@esbuild/darwin-x64": "0.25.11",
- "@esbuild/freebsd-arm64": "0.25.11",
- "@esbuild/freebsd-x64": "0.25.11",
- "@esbuild/linux-arm": "0.25.11",
- "@esbuild/linux-arm64": "0.25.11",
- "@esbuild/linux-ia32": "0.25.11",
- "@esbuild/linux-loong64": "0.25.11",
- "@esbuild/linux-mips64el": "0.25.11",
- "@esbuild/linux-ppc64": "0.25.11",
- "@esbuild/linux-riscv64": "0.25.11",
- "@esbuild/linux-s390x": "0.25.11",
- "@esbuild/linux-x64": "0.25.11",
- "@esbuild/netbsd-arm64": "0.25.11",
- "@esbuild/netbsd-x64": "0.25.11",
- "@esbuild/openbsd-arm64": "0.25.11",
- "@esbuild/openbsd-x64": "0.25.11",
- "@esbuild/openharmony-arm64": "0.25.11",
- "@esbuild/sunos-x64": "0.25.11",
- "@esbuild/win32-arm64": "0.25.11",
- "@esbuild/win32-ia32": "0.25.11",
- "@esbuild/win32-x64": "0.25.11"
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/escape-string-regexp": {
@@ -1944,21 +2091,20 @@
}
},
"node_modules/eslint": {
- "version": "9.38.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
- "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"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.1",
- "@eslint/core": "^0.16.0",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.38.0",
- "@eslint/plugin-kit": "^0.4.0",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -2020,17 +2166,64 @@
"eslint": ">=7.0.0"
}
},
- "node_modules/eslint-plugin-googleappsscript": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.5.tgz",
- "integrity": "sha512-pzGdRAoGzjefsGs1ct1MSip9RbBn24xJd/CxD2bszDU3HkuFYenAg/tpTGQQSb2hYMre0uL/H3IIGhN4qSnQTA==",
+ "node_modules/eslint-plugin-jsdoc": {
+ "version": "62.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.5.2.tgz",
+ "integrity": "sha512-n4plQz9u6xoX0QemOsBjU8S/V6XGRoBsad8v56Q9sEOKrcZTh489ld0qi7ONLNZsSlH0GBSi513DBvyavFckeQ==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-3-Clause",
"dependencies": {
- "requireindex": "~1.1.0"
+ "@es-joy/jsdoccomment": "~0.84.0",
+ "@es-joy/resolve.exports": "1.2.0",
+ "are-docs-informative": "^0.0.2",
+ "comment-parser": "1.4.5",
+ "debug": "^4.4.3",
+ "escape-string-regexp": "^4.0.0",
+ "espree": "^11.1.0",
+ "esquery": "^1.7.0",
+ "html-entities": "^2.6.0",
+ "object-deep-merge": "^2.0.0",
+ "parse-imports-exports": "^0.2.4",
+ "semver": "^7.7.3",
+ "spdx-expression-parse": "^4.0.0",
+ "to-valid-identifier": "^1.0.0"
},
"engines": {
- "node": ">=0.10.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
+ "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-jsdoc/node_modules/espree": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz",
+ "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-scope": {
@@ -2063,33 +2256,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/@eslint/core": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
- "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
- "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/node_modules/@eslint/plugin-kit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
- "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.16.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -2109,9 +2275,9 @@
}
},
"node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -2181,36 +2347,6 @@
"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",
@@ -2225,16 +2361,6 @@
"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/fault": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
@@ -2249,30 +2375,35 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "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==",
+ "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",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
"engines": {
- "node": ">=16.0.0"
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
}
},
- "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==",
+ "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": {
- "to-regex-range": "^5.0.1"
+ "flat-cache": "^4.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=16.0.0"
}
},
"node_modules/find-up": {
@@ -2370,13 +2501,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "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",
@@ -2387,6 +2511,30 @@
"node": ">=8"
}
},
+ "node_modules/html-entities": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+ "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT"
+ },
+ "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/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -2463,16 +2611,6 @@
"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",
@@ -2480,10 +2618,56 @@
"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-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": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "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": {
@@ -2493,6 +2677,16 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdoc-type-pratt-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.1.1.tgz",
+ "integrity": "sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -2582,6 +2776,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "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/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@@ -2838,16 +3060,6 @@
"url": "https://opencollective.com/unified"
}
},
- "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/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -3456,24 +3668,10 @@
],
"license": "MIT"
},
- "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/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3516,6 +3714,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-deep-merge": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.0.tgz",
+ "integrity": "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3579,6 +3795,23 @@
"node": ">=6"
}
},
+ "node_modules/parse-imports-exports": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz",
+ "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse-statements": "1.0.11"
+ }
+ },
+ "node_modules/parse-statements": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz",
+ "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3614,13 +3847,13 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "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": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -3666,9 +3899,9 @@
}
},
"node_modules/prettier": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
- "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
+ "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3691,41 +3924,23 @@
"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/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
- "node_modules/requireindex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
- "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==",
+ "node_modules/reserved-identifiers": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz",
+ "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=0.10.5"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-from": {
@@ -3738,21 +3953,10 @@
"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/rollup": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
- "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3766,55 +3970,34 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.52.5",
- "@rollup/rollup-android-arm64": "4.52.5",
- "@rollup/rollup-darwin-arm64": "4.52.5",
- "@rollup/rollup-darwin-x64": "4.52.5",
- "@rollup/rollup-freebsd-arm64": "4.52.5",
- "@rollup/rollup-freebsd-x64": "4.52.5",
- "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
- "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
- "@rollup/rollup-linux-arm64-gnu": "4.52.5",
- "@rollup/rollup-linux-arm64-musl": "4.52.5",
- "@rollup/rollup-linux-loong64-gnu": "4.52.5",
- "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
- "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
- "@rollup/rollup-linux-riscv64-musl": "4.52.5",
- "@rollup/rollup-linux-s390x-gnu": "4.52.5",
- "@rollup/rollup-linux-x64-gnu": "4.52.5",
- "@rollup/rollup-linux-x64-musl": "4.52.5",
- "@rollup/rollup-openharmony-arm64": "4.52.5",
- "@rollup/rollup-win32-arm64-msvc": "4.52.5",
- "@rollup/rollup-win32-ia32-msvc": "4.52.5",
- "@rollup/rollup-win32-x64-gnu": "4.52.5",
- "@rollup/rollup-win32-x64-msvc": "4.52.5",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"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/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -3868,6 +4051,31 @@
"node": ">=0.10.0"
}
},
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.22",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz",
+ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -3876,9 +4084,9 @@
"license": "MIT"
},
"node_modules/std-env": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
- "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"dev": true,
"license": "MIT"
},
@@ -3916,11 +4124,14 @@
"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==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/tinyglobby": {
"version": "0.2.15",
@@ -3939,38 +4150,6 @@
"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",
- "peer": true,
- "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",
@@ -3981,23 +4160,27 @@
"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==",
+ "node_modules/to-valid-identifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz",
+ "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-number": "^7.0.0"
+ "@sindresorhus/base62": "^1.0.0",
+ "reserved-identifiers": "^1.0.0"
},
"engines": {
- "node": ">=8.0"
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"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==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4024,7 +4207,6 @@
"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",
"peer": true,
"bin": {
@@ -4036,16 +4218,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
- "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz",
+ "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.46.2",
- "@typescript-eslint/parser": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2",
- "@typescript-eslint/utils": "8.46.2"
+ "@typescript-eslint/eslint-plugin": "8.53.0",
+ "@typescript-eslint/parser": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4136,14 +4318,13 @@
}
},
"node_modules/vite": {
- "version": "7.1.12",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
- "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "esbuild": "^0.25.0",
+ "esbuild": "^0.27.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
@@ -4211,61 +4392,29 @@
}
}
},
- "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/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/vitest": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.4.tgz",
- "integrity": "sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==",
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
+ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.0.4",
- "@vitest/mocker": "4.0.4",
- "@vitest/pretty-format": "4.0.4",
- "@vitest/runner": "4.0.4",
- "@vitest/snapshot": "4.0.4",
- "@vitest/spy": "4.0.4",
- "@vitest/utils": "4.0.4",
- "debug": "^4.4.3",
+ "@vitest/expect": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "@vitest/pretty-format": "4.0.18",
+ "@vitest/runner": "4.0.18",
+ "@vitest/snapshot": "4.0.18",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
- "magic-string": "^0.30.19",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
- "std-env": "^3.9.0",
+ "std-env": "^3.10.0",
"tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
+ "tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3",
"vite": "^6.0.0 || ^7.0.0",
@@ -4282,12 +4431,12 @@
},
"peerDependencies": {
"@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
+ "@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.0.4",
- "@vitest/browser-preview": "4.0.4",
- "@vitest/browser-webdriverio": "4.0.4",
- "@vitest/ui": "4.0.4",
+ "@vitest/browser-playwright": "4.0.18",
+ "@vitest/browser-preview": "4.0.18",
+ "@vitest/browser-webdriverio": "4.0.18",
+ "@vitest/ui": "4.0.18",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4295,7 +4444,7 @@
"@edge-runtime/vm": {
"optional": true
},
- "@types/debug": {
+ "@opentelemetry/api": {
"optional": true
},
"@types/node": {
@@ -4321,19 +4470,6 @@
}
}
},
- "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/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 8497edf..ce0d391 100644
--- a/package.json
+++ b/package.json
@@ -2,15 +2,24 @@
"name": "bootgs",
"version": "1.1.0",
"description": "Boot Framework for Google Apps Script™ projects.",
- "main": "src/index.ts",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
"scripts": {
- "maint": "bash ./scripts/maintenance.sh",
+ "prepare": "husky",
"dev": "vitest",
- "format": "prettier --write .",
- "lint": "eslint --fix .",
+ "type:check": "tsc --noEmit",
+ "format": "prettier --check .",
+ "format:fix": "prettier --write .",
+ "lint": "eslint .",
+ "lint:fix": "eslint --fix .",
"test": "vitest run",
- "build": "rm -rf dist/* && tsc",
- "prepare": "husky"
+ "test:unit": "vitest run test/unit",
+ "test:integration": "vitest run test/integration",
+ "test:cov": "npx vitest run --coverage",
+ "build": "rm -rf dist/* && tsc -b --force"
},
"repository": {
"type": "git",
@@ -47,21 +56,23 @@
"url": "https://buymeacoffee.com/MaksymStoianov"
}
],
- "peerDependencies": {
- "typescript": "^5.9.3"
- },
"devDependencies": {
- "@eslint/json": "^0.13.2",
- "@eslint/markdown": "^7.5.0",
- "@types/google-apps-script": "^2.0.7",
- "@types/node": "^24.9.1",
- "eslint": "^9.38.0",
+ "@eslint/json": "^0.14.0",
+ "@eslint/markdown": "^7.5.1",
+ "@types/google-apps-script": "^2.0.8",
+ "@types/node": "^25.0.9",
+ "@vitest/coverage-v8": "^4.0.18",
+ "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-googleappsscript": "^1.0.5",
+ "eslint-plugin-jsdoc": "^62.5.2",
"husky": "^9.1.7",
- "prettier": "^3.6.2",
- "typescript-eslint": "^8.46.2",
- "vitest": "^4.0.4"
+ "prettier": "^3.8.0",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.53.0",
+ "vitest": "^4.0.17"
+ },
+ "engines": {
+ "node": ">=22.14.0"
},
"dependencies": {
"apps-script-utils": "github:MaksymStoianov/apps-script-utils#main",
diff --git a/scripts/maintenance.sh b/scripts/maintenance.sh
deleted file mode 100644
index 39656a7..0000000
--- a/scripts/maintenance.sh
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/bin/bash
-
-LOG_TAG="[maintanance:script]"
-
-echo "$LOG_TAG: Starting repository maintenance."
-
-
-# Переход в корневой каталог
-# Используем команду git для поиска корня проекта, где лежит package.json
-PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
-if [ $? -ne 0 ]; then
- echo " ✗ Error: Cannot find project root (not a Git repository)."
- exit 1
-fi
-cd "$PROJECT_ROOT" || exit 1
-echo "$LOG_TAG: Working directory set to $(pwd)"
-echo ""
-
-
-# Проверка на наличие незакоммиченных изменений
-echo "$LOG_TAG: Checking for uncommitted changes..."
-if ! git diff --quiet --exit-code; then
- echo " ✗ Error: Uncommitted changes detected. Please commit or stash them first."
- exit 1
-fi
-echo " ✓ Done."
-echo ""
-
-
-# Git синхронизация
-echo "$LOG_TAG: Syncing with 'main' branch..."
-git checkout main
-if [ $? -ne 0 ]; then
- echo " ✗ Error: Failed to checkout 'main' branch."
- exit 1
-fi
-
-git pull
-if [ $? -ne 0 ]; then
- echo " ✗ Error: Git pull failed."
- exit 1
-fi
-echo " ✓ Done."
-echo ""
-
-
-# Установка зависимостей
-echo "$LOG_TAG: Installing dependencies..."
-npm install
-if [ $? -ne 0 ]; then
- echo " ✗ Error: npm install failed."
- exit 1
-fi
-echo " ✓ Done."
-echo ""
-
-
-# Очистка кеша
-echo "$LOG_TAG: Cleaning npm cache..."
-npm cache clean --force
-echo " ✓ Done."
-echo ""
-
-
-# Запуск аудита безопасности
-echo "$LOG_TAG: Running npm audit fix..."
-npm audit fix
-if [ $? -ne 0 ]; then
- # audit fix может вернуть 1, если есть High/Critical уязвимости,
- # которые не удалось исправить автоматически. Мы продолжаем,
- # но предупреждаем, что ручной фикс может потребоваться.
- echo " ✗ Warning: npm audit fix completed, but unfixable vulnerabilities may remain."
-fi
-
-# Проверка оставшихся уязвимостей
-npm audit --audit-level=high
-if [ $? -ne 0 ]; then
- echo " ✗ Error: Unfixable high-severity vulnerabilities found."
-fi
-echo " ✓ Done."
-echo ""
-
-
-# Проверка типов TypeScript (без компиляции)
-echo "$LOG_TAG: Running TypeScript check (tsc --noEmit)..."
-tsc --noEmit
-if [ $? -ne 0 ]; then
- echo " ✗ Error: TypeScript check failed."
- exit 1
-fi
-echo " ✓ Done."
-echo ""
-
-
-# Проверка версий
-echo "$LOG_TAG: 5. Checking for outdated packages..."
-npm outdated
-
-if [ $? -ne 0 ]; then
- echo " ! Warning: Outdated packages found (see list above)."
- echo " ---"
- echo " For automated updating of versions in package.json, use the command:"
- echo " npx npm-check-updates -u"
- echo " After that, run 'npm install'."
- echo " ---"
-else
- echo " ✓ No outdated packages found in package.json."
-fi
-echo " ✓ Done with version check."
-echo ""
-
-
-# Проверка лицензий
-echo "$LOG_TAG: Running license check..."
-# Установка license-checker, если не установлен
-if ! command -v license-checker &> /dev/null
-then
- echo "$LOG_TAG: Installing license-checker globally..."
- npm install -g license-checker
- if [ $? -ne 0 ]; then
- echo " ✗ Error: Failed to install license-checker."
- exit 1
- fi
-fi
-
-license-checker --production
-if [ $? -ne 0 ]; then
- echo " ✗ Error: License checker failed (usually due to missing packages in path)."
-fi
-echo " ✓ Done."
-echo ""
-
-
-echo "$LOG_TAG: Maintenance completed successfully."
-exit 0
\ No newline at end of file
diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh
index 7043489..41a8275 100755
--- a/scripts/pre-commit.sh
+++ b/scripts/pre-commit.sh
@@ -57,7 +57,7 @@ echo ""
# Линтинг
echo "$LOG_TAG: Running lint..."
-npm run lint
+npm run lint:fix
if [ $? -ne 0 ]; then
echo " ✗ Error: Linting failed."
exit 1
@@ -68,7 +68,7 @@ echo ""
# Форматирование
echo "$LOG_TAG: Running format..."
-npm run format
+npm run format:fix
if [ $? -ne 0 ]; then
echo " ✗ Error: Formatting failed."
exit 1
diff --git a/scripts/pre-push.sh b/scripts/pre-push.sh
index 4189413..9d71372 100755
--- a/scripts/pre-push.sh
+++ b/scripts/pre-push.sh
@@ -57,7 +57,7 @@ echo ""
# Линтинг
echo "$LOG_TAG: Running lint..."
-npm run lint
+npm run lint:fix
if [ $? -ne 0 ]; then
echo " ✗ Error: Linting failed."
exit 1
@@ -68,7 +68,7 @@ echo ""
# Форматирование
echo "$LOG_TAG: Running format..."
-npm run format
+npm run format:fix
if [ $? -ne 0 ]; then
echo " ✗ Error: Formatting failed."
exit 1
diff --git a/src/App.ts b/src/App.ts
deleted file mode 100644
index 8d06317..0000000
--- a/src/App.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-import { isFunctionLike, isString } from "apps-script-utils";
-import {
- APPSSCRIPT_EVENT_METADATA,
- APPSSCRIPT_OPTIONS_METADATA,
- CONTROLLER_TYPE_METADATA
-} from "./config/constants";
-import {
- AppConfig,
- AppsScriptEventType,
- HttpHeaders,
- HttpRequest,
- HttpStatus,
- Newable,
- RequestMethod,
- RouteMetadata
-} from "./types";
-import {
- buildMethodParams,
- checkEventFilters,
- createRequest,
- createResponse,
- extractPathParams,
- isController,
- pathMatch,
- resolve,
- RouterExplorer,
- wrapResponse
-} from "./utils";
-
-/**
- * The main application class responsible for handling various types of Google Apps Script events.
- * This class implements the Singleton pattern, ensuring only one instance exists throughout the application lifecycle.
- *
- * @environment `Google Apps Script`
- */
-export class App {
- private static instance: App | null = null;
-
- private readonly _controllers = new Map();
- private readonly _providers = new Map();
- private readonly _routes: RouteMetadata[] = [];
-
- /**
- * Constructor for the App class.
- * It applies the Singleton pattern, ensuring that only one instance of App exists.
- *
- * @param config - The application configuration, including the controllers and providers to be registered.
- */
- constructor({ controllers, providers }: AppConfig = {}) {
- if (App.instance) {
- return App.instance;
- }
-
- for (const controller of controllers ?? []) {
- if (!isFunctionLike(controller)) continue;
- if (!isController(controller)) continue;
-
- this._controllers.set(controller, null);
- }
-
- this._routes = RouterExplorer.explore(this._controllers);
-
- for (const provider of providers ?? []) {
- if (!isFunctionLike(provider)) continue;
-
- this._providers.set(provider, null);
- }
-
- App.instance = this;
- }
-
- /**
- * Static method to create or retrieve the singleton instance of the App.
- * This method is used to initialize the application.
- *
- * @param config - The application configuration, including controllers and providers.
- * @returns The single instance of the App.
- */
- static create(config?: AppConfig | null | undefined): App {
- return new App(config ?? {});
- }
-
- /**
- * Handles the add-on installation event (`onInstall`).
- * It scans for controllers decorated with `@Install()` and invokes their respective methods.
- *
- * @param event - The installation event object.
- * @returns
- * @see onOpen
- * @see onEdit
- * @see onChange
- * @see onSelectionChange
- * @see onFormSubmit
- * @see doGet
- * @see doPost
- */
- onInstall(event: GoogleAppsScript.Events.AddonOnInstall): void {
- return this.on(AppsScriptEventType.INSTALL, event);
- }
-
- /**
- * Handles the _document_ | _spreadsheet_ | _presentation_ | _form_ open event (`onOpen`).
- * It scans for controllers decorated with `@Open()` and invokes their respective methods.
- *
- * @param event - The open event object.
- * @returns
- * @see onInstall
- * @see onEdit
- * @see onChange
- * @see onSelectionChange
- * @see onFormSubmit
- * @see doGet
- * @see doPost
- */
- onOpen(
- event:
- | GoogleAppsScript.Events.DocsOnOpen
- | GoogleAppsScript.Events.SlidesOnOpen
- | GoogleAppsScript.Events.SheetsOnOpen
- | GoogleAppsScript.Events.FormsOnOpen
- ): void {
- return this.on(AppsScriptEventType.OPEN, event);
- }
-
- /**
- * Handles the spreadsheet edit event (`onEdit`).
- * It scans for controllers decorated with `@Edit()` and invokes their respective methods.
- *
- * @param event - The edit event object.
- * @returns
- * @see onInstall
- * @see onOpen
- * @see onChange
- * @see onSelectionChange
- * @see onFormSubmit
- * @see doGet
- * @see doPost
- */
- onEdit(event: GoogleAppsScript.Events.SheetsOnEdit): void {
- return this.on(AppsScriptEventType.EDIT, event);
- }
-
- /**
- * Handles the spreadsheet change event (`onChange`).
- * It scans for controllers decorated with `@Change()` and invokes their respective methods.
- *
- * @param event - The change event object.
- * @returns
- * @see onInstall
- * @see onOpen
- * @see onEdit
- * @see onSelectionChange
- * @see onFormSubmit
- * @see doGet
- * @see doPost
- */
- onChange(event: GoogleAppsScript.Events.SheetsOnChange): void {
- return this.on(AppsScriptEventType.CHANGE, event);
- }
-
- /**
- * Handles the spreadsheet selection change event (`onSelectionChange`).
- * It scans for controllers decorated with `@SelectionChange()` and invokes their respective methods.
- *
- * @param event - The selection change event object.
- * @returns
- * @see onInstall
- * @see onOpen
- * @see onEdit
- * @see onChange
- * @see onFormSubmit
- * @see doGet
- * @see doPost
- */
- onSelectionChange(
- event: GoogleAppsScript.Events.SheetsOnSelectionChange
- ): void {
- return this.on(AppsScriptEventType.SELECTION_CHANGE, event);
- }
-
- /**
- * Handles the form submission event (`onFormSubmit`).
- * It scans for controllers decorated with `@FormSubmit()` and invokes their respective methods.
- *
- * @param event - The form submission event object.
- * @returns
- * @see onInstall
- * @see onOpen
- * @see onEdit
- * @see onChange
- * @see onSelectionChange
- * @see doGet
- * @see doPost
- */
- onFormSubmit(event: GoogleAppsScript.Events.FormsOnFormSubmit): void {
- return this.on(AppsScriptEventType.FORM_SUBMIT, event);
- }
-
- /**
- * Handles HTTP GET requests (`doGet`).
- * It processes the incoming event, routes it to the appropriate controller method,
- * and returns a response suitable for Apps Script.
- *
- * @param event - The HTTP GET event object.
- * @returns The response to be returned by the Apps Script `doGet` function.
- * @see doPost
- * @see onInstall
- * @see onOpen
- * @see onEdit
- * @see onChange
- * @see onSelectionChange
- * @see onFormSubmit
- */
- doGet(
- event: GoogleAppsScript.Events.DoGet
- ):
- | string
- | object
- | null
- | GoogleAppsScript.Content.TextOutput
- | GoogleAppsScript.HTML.HtmlOutput {
- return this.do(RequestMethod.GET, event);
- }
-
- /**
- * Handles HTTP POST requests (`doPost`).
- * It processes the incoming event, routes it to the appropriate controller method,
- * and returns a response suitable for Apps Script.
- *
- * @param event - The HTTP POST event object.
- * @returns The response to be returned by the Apps Script `doPost` function.
- * @see doGet
- * @see onInstall
- * @see onOpen
- * @see onEdit
- * @see onChange
- * @see onSelectionChange
- * @see onFormSubmit
- */
- doPost(
- event: GoogleAppsScript.Events.DoPost
- ):
- | string
- | object
- | null
- | GoogleAppsScript.Content.TextOutput
- | GoogleAppsScript.HTML.HtmlOutput {
- return this.do(RequestMethod.POST, event);
- }
-
- /**
- * Handles incoming HTTP requests (both `doPost` and `doGet` events).
- * This is the core routing and request processing method for HTTP endpoints.
- * It creates a structured request, finds a matching route, resolves the controller,
- * builds method parameters, executes the handler, and wraps the response.
- *
- * @param method - The HTTP request method (GET, POST, PUT, DELETE, etc.).
- * @param event - The raw Apps Script HTTP event object.
- * @returns The response to be returned by the Apps Script HTTP entry point function.
- */
- do(
- method: RequestMethod,
- event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost
- ):
- | string
- | object
- | null
- | GoogleAppsScript.Content.TextOutput
- | GoogleAppsScript.HTML.HtmlOutput {
- const request: HttpRequest = createRequest(method, event);
- const headers: HttpHeaders = {};
-
- try {
- let route: RouteMetadata | null = null;
-
- if (request.url.pathname) {
- const pathname = decodeURI(request.url.pathname);
-
- route =
- this._routes.find(
- route =>
- route.method === request.method && pathMatch(route.path, pathname)
- ) || null;
- }
-
- if (!route) {
- return wrapResponse(
- request,
- createResponse(
- request,
- HttpStatus.NOT_FOUND,
- headers,
- `Not Found: Cannot ${request.method} ${request.url.pathname}`
- )
- );
- }
-
- const controllerInstance = resolve(
- this._controllers,
- this._providers,
- route.controller
- );
-
- const handler =
- controllerInstance[
- route.handler as keyof typeof controllerInstance
- ].bind(controllerInstance);
-
- if (!isFunctionLike(handler)) {
- throw new Error(
- `Method "${String(route.handler)}" in controller "${route.controller.name}" is not a callable function.`
- );
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const body = ((data, type): any => {
- if (!isString(data)) {
- return data;
- }
-
- if (type === "application/json") {
- try {
- return JSON.parse(data.trim());
- } catch (err: unknown) {
- console.warn("Failed to parse JSON:", err);
- }
- }
-
- return data;
- })(
- "postData" in event ? event?.postData?.contents : null,
- "postData" in event ? event?.postData?.type : "text/plain"
- );
-
- const args = buildMethodParams(
- controllerInstance,
- route.handler,
- {
- event,
- query: event.parameter,
- params: extractPathParams(route.path, request.url.pathname!),
- request,
- headers,
- body
- // TODO: add response
- },
- this._controllers,
- this._providers
- );
-
- const result = handler(...args);
- const response = createResponse(request, undefined, headers, result);
-
- return wrapResponse(request, response);
- } catch (err: unknown) {
- if (err instanceof Error) {
- console.error(err?.stack || err?.message);
- } else {
- console.error(String(err));
- }
-
- const response = createResponse(
- request,
- HttpStatus.INTERNAL_SERVER_ERROR,
- headers,
- String(err)
- );
-
- return wrapResponse(request, response);
- }
- }
-
- /**
- * A universal handler for Google Apps Script events (excluding HTTP events).
- * This method scans registered controllers and their methods for corresponding event decorators.
- *
- * It iterates through all known controllers, resolves their instances, and then examines each method to find those decorated with specific Apps Script event types (e.g., `@Open`, `@Edit`).
- * If a method's event type matches the current `eventType` and passes any defined filters, the method's arguments are built, and the method is executed.
- *
- * @param eventType - The expected type of event. This is used to match with the metadata placed by event decorators on controller methods.
- * @param event - The raw Apps Script event object. The structure of this object varies based on the specific Apps Script trigger that fired.
- * @returns
- */
- on(
- eventType: AppsScriptEventType,
- event:
- | GoogleAppsScript.Events.AddonOnInstall
- | GoogleAppsScript.Events.DocsOnOpen
- | GoogleAppsScript.Events.SlidesOnOpen
- | GoogleAppsScript.Events.SheetsOnOpen
- | GoogleAppsScript.Events.FormsOnOpen
- | GoogleAppsScript.Events.SheetsOnEdit
- | GoogleAppsScript.Events.SheetsOnChange
- | GoogleAppsScript.Events.SheetsOnSelectionChange
- | GoogleAppsScript.Events.FormsOnFormSubmit
- ): void {
- for (const controller of this._controllers.keys()) {
- const controllerType: string | null =
- Reflect.getMetadata(CONTROLLER_TYPE_METADATA, controller) || null;
-
- const isAppsScriptController =
- isString(controllerType) && controllerType.startsWith("appsscript");
-
- if (!isAppsScriptController) {
- continue;
- }
-
- const controllerInstance = resolve(
- this._controllers,
- this._providers,
- controller
- );
-
- const prototype = Object.getPrototypeOf(controllerInstance);
-
- const methodNames: string[] = [];
- let currentProto = prototype;
- while (currentProto && currentProto !== Object.prototype) {
- Object.getOwnPropertyNames(currentProto).forEach(name => {
- if (name !== "constructor" && isFunctionLike(currentProto[name])) {
- methodNames.push(name);
- }
- });
- currentProto = Object.getPrototypeOf(currentProto);
- }
-
- for (const methodName of methodNames) {
- const methodFunction = prototype?.[methodName];
-
- const methodEventType = Reflect.getMetadata(
- APPSSCRIPT_EVENT_METADATA,
- methodFunction
- );
-
- if (methodEventType !== eventType) {
- continue;
- }
-
- const methodOptions = Reflect.getMetadata(
- APPSSCRIPT_OPTIONS_METADATA,
- methodFunction
- );
-
- const matchesFilters = checkEventFilters(
- eventType,
- event,
- methodOptions
- );
-
- if (!matchesFilters) {
- continue;
- }
-
- const handler =
- controllerInstance[
- methodName as keyof typeof controllerInstance
- ].bind(controllerInstance);
-
- if (!isFunctionLike(handler)) {
- console.warn(
- "Method '%s' in controller '%s' is not a callable function and was skipped during event handling.",
- methodName,
- controller.name
- );
- continue;
- }
-
- const args = buildMethodParams(
- controllerInstance,
- methodName,
- {
- event
- },
- this._controllers,
- this._providers
- );
-
- try {
- handler(...args);
- } catch (err: unknown) {
- console.error(
- "Error:",
- err instanceof Error ? err.stack : String(err)
- );
- }
- }
- }
- }
-}
diff --git a/src/controller/BootApplication.ts b/src/controller/BootApplication.ts
new file mode 100644
index 0000000..6dd03b7
--- /dev/null
+++ b/src/controller/BootApplication.ts
@@ -0,0 +1,98 @@
+import { ApplicationConfig, InjectionToken, Newable } from "../domain/types";
+import { AppsScriptEventType, RequestMethod } from "../domain/enums";
+import {
+ EventDispatcher,
+ RequestFactory,
+ Resolver,
+ ResponseBuilder,
+ Router,
+ RouterExplorer
+} from "../service";
+
+export class BootApplication {
+ private readonly _controllers = new Map();
+ private readonly _providers = new Map();
+ private readonly _resolver: Resolver;
+ private readonly _router: Router;
+ private readonly _requestFactory = new RequestFactory();
+ private readonly _responseBuilder = new ResponseBuilder();
+ private readonly _eventDispatcher: EventDispatcher;
+
+ constructor(config: ApplicationConfig) {
+ (config.controllers || []).forEach((c) => this._controllers.set(c, null));
+
+ (config.providers || []).forEach((p) => {
+ if ("provide" in p) {
+ if ("useValue" in p) {
+ this._providers.set(p.provide, p.useValue);
+ } else if ("useClass" in p) {
+ this._providers.set(p.provide, null); // Will be resolved later
+ } else if ("useFactory" in p) {
+ // TODO: implement factory providers
+ this._providers.set(p.provide, null);
+ } else if ("useExisting" in p) {
+ // TODO: implement existing providers
+ this._providers.set(p.provide, null);
+ }
+ } else {
+ this._providers.set(p, null);
+ }
+ });
+
+ this._resolver = new Resolver(this._controllers, this._providers);
+
+ const explorer = new RouterExplorer();
+
+ const routes = explorer.explore(this._controllers);
+
+ this._router = new Router(this._resolver, routes);
+
+ this._eventDispatcher = new EventDispatcher(this._resolver, this._controllers);
+ }
+
+ public async doGet(event: GoogleAppsScript.Events.DoGet) {
+ return this.handleHttpRequest(RequestMethod.GET, event);
+ }
+
+ public async doPost(event: GoogleAppsScript.Events.DoPost) {
+ return this.handleHttpRequest(RequestMethod.POST, event);
+ }
+
+ public async onInstall(event: GoogleAppsScript.Events.AddonOnInstall) {
+ await this._eventDispatcher.dispatch(AppsScriptEventType.INSTALL, event);
+ }
+
+ public async onOpen(event: GoogleAppsScript.Events.AppsScriptEvent) {
+ await this._eventDispatcher.dispatch(AppsScriptEventType.OPEN, event);
+ }
+
+ public async onEdit(event: GoogleAppsScript.Events.SheetsOnEdit) {
+ await this._eventDispatcher.dispatch(AppsScriptEventType.EDIT, event);
+ }
+
+ public async onChange(event: GoogleAppsScript.Events.SheetsOnChange) {
+ await this._eventDispatcher.dispatch(AppsScriptEventType.CHANGE, event);
+ }
+
+ // TODO: onSelectionChange
+ // public async onSelectionChange(event: GoogleAppsScript.Events.SheetsOnSelectionChange) {
+ // await this.eventDispatcher.dispatch(AppsScriptEventType.SELECTION_CHANGE, event);
+ // }
+
+ public async onFormSubmit(event: GoogleAppsScript.Events.FormsOnFormSubmit) {
+ await this._eventDispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, event);
+ }
+
+ private async handleHttpRequest(
+ method: RequestMethod,
+ event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost
+ ) {
+ const request = this._requestFactory.create(method, event);
+
+ const response = await this._router.handle(request, event, (req, status, headers, data) =>
+ this._responseBuilder.create(req, status, headers, data)
+ );
+
+ return this._responseBuilder.wrap(request, response);
+ }
+}
diff --git a/src/controller/BootApplicationFactory.ts b/src/controller/BootApplicationFactory.ts
new file mode 100644
index 0000000..b0f387a
--- /dev/null
+++ b/src/controller/BootApplicationFactory.ts
@@ -0,0 +1,14 @@
+import { ApplicationConfig } from "../domain/types";
+import { BootApplication } from "../controller";
+
+export class BootApplicationFactory {
+ /**
+ * Creates an instance of BootApplication.
+ *
+ * @param {ApplicationConfig} config - The application configuration.
+ * @returns {BootApplication} An instance of BootApplication.
+ */
+ public static create(config: ApplicationConfig): BootApplication {
+ return new BootApplication(config);
+ }
+}
diff --git a/src/controller/decorators/Controller.ts b/src/controller/decorators/Controller.ts
new file mode 100644
index 0000000..c5c163e
--- /dev/null
+++ b/src/controller/decorators/Controller.ts
@@ -0,0 +1,27 @@
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "../../domain/constants";
+
+/**
+ * Controller options.
+ */
+export interface ControllerOptions {
+ basePath?: string;
+}
+
+/**
+ * Decorator that marks a class as a controller.
+ *
+ * @param {string} type - Controller type (e.g., 'http', 'sheets').
+ * @param {ControllerOptions} [options] - Controller options.
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function Controller(type: string, options: ControllerOptions = {}): ClassDecorator {
+ return (target: object) => {
+ Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
+ Reflect.defineMetadata(CONTROLLER_TYPE_METADATA, type, target);
+ Reflect.defineMetadata(CONTROLLER_OPTIONS_METADATA, options, target);
+ };
+}
diff --git a/src/controller/decorators/Entity.ts b/src/controller/decorators/Entity.ts
new file mode 100644
index 0000000..75e0ff1
--- /dev/null
+++ b/src/controller/decorators/Entity.ts
@@ -0,0 +1,14 @@
+import { ENTITY_WATERMARK } from "../../domain/constants";
+
+/**
+ * Decorator that marks a class as an entity.
+ *
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function Entity(): ClassDecorator {
+ return (target: object) => {
+ // In a GAS context, this might be used to map classes to Spreadsheet ranges or other storage.
+ // For now, it serves as a semantic marker.
+ Reflect.defineMetadata(ENTITY_WATERMARK, true, target);
+ };
+}
diff --git a/src/controller/decorators/HttpController.ts b/src/controller/decorators/HttpController.ts
new file mode 100644
index 0000000..af9526b
--- /dev/null
+++ b/src/controller/decorators/HttpController.ts
@@ -0,0 +1,11 @@
+import { Controller } from "../../controller/decorators";
+
+/**
+ * Decorator that marks a class as an HTTP controller.
+ *
+ * @param {string} [basePath] - The base path for all routes in this controller.
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function HttpController(basePath: string = "/"): ClassDecorator {
+ return Controller("http", { basePath: basePath === undefined ? "/" : basePath });
+}
diff --git a/src/controller/decorators/Inject.ts b/src/controller/decorators/Inject.ts
new file mode 100644
index 0000000..3b0824d
--- /dev/null
+++ b/src/controller/decorators/Inject.ts
@@ -0,0 +1,28 @@
+import { INJECT_TOKENS_METADATA } from "../../domain/constants";
+import { InjectTokenDefinition, Newable } from "../../domain/types";
+import { assignInjectMetadata } from "../../repository";
+
+/**
+ * A parameter decorator used to explicitly specify an injection token for a dependency.
+ *
+ * @param {Newable | string | symbol} [token] - The injection token that the DI container will use to resolve the dependency.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export function Inject(token?: Newable | string | symbol): ParameterDecorator {
+ return (target, propertyKey, parameterIndex) => {
+ const metadataTarget = target;
+
+ const existing: Record =
+ (propertyKey
+ ? Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget, propertyKey)
+ : Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget)) || {};
+
+ const updatedTokens = assignInjectMetadata(existing, parameterIndex, token);
+
+ if (propertyKey) {
+ Reflect.defineMetadata(INJECT_TOKENS_METADATA, updatedTokens, metadataTarget, propertyKey);
+ } else {
+ Reflect.defineMetadata(INJECT_TOKENS_METADATA, updatedTokens, metadataTarget);
+ }
+ };
+}
diff --git a/src/controller/decorators/Injectable.ts b/src/controller/decorators/Injectable.ts
new file mode 100644
index 0000000..90a84c6
--- /dev/null
+++ b/src/controller/decorators/Injectable.ts
@@ -0,0 +1,12 @@
+import { INJECTABLE_WATERMARK } from "../../domain/constants";
+
+/**
+ * Decorator that marks a class as injectable (a provider).
+ *
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function Injectable(): ClassDecorator {
+ return (target: object) => {
+ Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
+ };
+}
diff --git a/src/controller/decorators/Repository.ts b/src/controller/decorators/Repository.ts
new file mode 100644
index 0000000..e7f9f7a
--- /dev/null
+++ b/src/controller/decorators/Repository.ts
@@ -0,0 +1,10 @@
+import { Injectable } from "../../controller/decorators";
+
+/**
+ * Decorator that marks a class as a repository.
+ *
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function Repository(): ClassDecorator {
+ return Injectable();
+}
diff --git a/src/controller/decorators/RestController.ts b/src/controller/decorators/RestController.ts
new file mode 100644
index 0000000..942bcaf
--- /dev/null
+++ b/src/controller/decorators/RestController.ts
@@ -0,0 +1,3 @@
+import { HttpController } from "../../controller/decorators";
+
+export const RestController = HttpController;
diff --git a/src/controller/decorators/Service.ts b/src/controller/decorators/Service.ts
new file mode 100644
index 0000000..74568a3
--- /dev/null
+++ b/src/controller/decorators/Service.ts
@@ -0,0 +1,10 @@
+import { Injectable } from "../../controller/decorators";
+
+/**
+ * Decorator that marks a class as a service.
+ *
+ * @returns {ClassDecorator} A class decorator.
+ */
+export function Service(): ClassDecorator {
+ return Injectable();
+}
diff --git a/src/controller/decorators/appsscript/OnChange.ts b/src/controller/decorators/appsscript/OnChange.ts
new file mode 100644
index 0000000..39c3923
--- /dev/null
+++ b/src/controller/decorators/appsscript/OnChange.ts
@@ -0,0 +1,4 @@
+import { AppsScriptEventType } from "../../../domain/enums";
+import { createAppsScriptDecorator } from "../../../repository";
+
+export const OnChange = createAppsScriptDecorator(AppsScriptEventType.CHANGE);
diff --git a/src/controller/decorators/appsscript/OnEdit.ts b/src/controller/decorators/appsscript/OnEdit.ts
new file mode 100644
index 0000000..1248761
--- /dev/null
+++ b/src/controller/decorators/appsscript/OnEdit.ts
@@ -0,0 +1,4 @@
+import { AppsScriptEventType } from "../../../domain/enums";
+import { createAppsScriptDecorator } from "../../../repository";
+
+export const OnEdit = createAppsScriptDecorator(AppsScriptEventType.EDIT);
diff --git a/src/controller/decorators/appsscript/OnFormSubmit.ts b/src/controller/decorators/appsscript/OnFormSubmit.ts
new file mode 100644
index 0000000..be50c0a
--- /dev/null
+++ b/src/controller/decorators/appsscript/OnFormSubmit.ts
@@ -0,0 +1,4 @@
+import { AppsScriptEventType } from "../../../domain/enums";
+import { createAppsScriptDecorator } from "../../../repository";
+
+export const OnFormSubmit = createAppsScriptDecorator(AppsScriptEventType.FORM_SUBMIT);
diff --git a/src/controller/decorators/appsscript/OnInstall.ts b/src/controller/decorators/appsscript/OnInstall.ts
new file mode 100644
index 0000000..20060db
--- /dev/null
+++ b/src/controller/decorators/appsscript/OnInstall.ts
@@ -0,0 +1,4 @@
+import { AppsScriptEventType } from "../../../domain/enums";
+import { createAppsScriptDecorator } from "../../../repository";
+
+export const OnInstall = createAppsScriptDecorator(AppsScriptEventType.INSTALL);
diff --git a/src/controller/decorators/appsscript/OnOpen.ts b/src/controller/decorators/appsscript/OnOpen.ts
new file mode 100644
index 0000000..542be33
--- /dev/null
+++ b/src/controller/decorators/appsscript/OnOpen.ts
@@ -0,0 +1,4 @@
+import { AppsScriptEventType } from "../../../domain/enums";
+import { createAppsScriptDecorator } from "../../../repository";
+
+export const OnOpen = createAppsScriptDecorator(AppsScriptEventType.OPEN);
diff --git a/src/controller/decorators/appsscript/index.ts b/src/controller/decorators/appsscript/index.ts
new file mode 100644
index 0000000..1b82b8f
--- /dev/null
+++ b/src/controller/decorators/appsscript/index.ts
@@ -0,0 +1,5 @@
+export * from "./OnChange";
+export * from "./OnEdit";
+export * from "./OnFormSubmit";
+export * from "./OnInstall";
+export * from "./OnOpen";
diff --git a/src/controller/decorators/index.ts b/src/controller/decorators/index.ts
new file mode 100644
index 0000000..34fe2d9
--- /dev/null
+++ b/src/controller/decorators/index.ts
@@ -0,0 +1,12 @@
+export * from "./appsscript";
+export * from "./params";
+export * from "./routing";
+
+export * from "./Controller";
+export * from "./Entity";
+export * from "./HttpController";
+export * from "./Inject";
+export * from "./Injectable";
+export * from "./Repository";
+export * from "./RestController";
+export * from "./Service";
diff --git a/src/controller/decorators/params/Body.ts b/src/controller/decorators/params/Body.ts
new file mode 100644
index 0000000..532b639
--- /dev/null
+++ b/src/controller/decorators/params/Body.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting the full request body.
+ *
+ * @param {string} [key] - The name of a key to extract a specific value from the request body.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Body = createParamDecorator(ParamSource.BODY);
diff --git a/src/controller/decorators/params/Event.ts b/src/controller/decorators/params/Event.ts
new file mode 100644
index 0000000..e171dbe
--- /dev/null
+++ b/src/controller/decorators/params/Event.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting the raw Apps Script event object.
+ *
+ * @param {string} [key] - The name of a key to extract a specific value from the event object.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Event = createParamDecorator(ParamSource.EVENT);
diff --git a/src/controller/decorators/params/Headers.ts b/src/controller/decorators/params/Headers.ts
new file mode 100644
index 0000000..cd85272
--- /dev/null
+++ b/src/controller/decorators/params/Headers.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting request headers.
+ *
+ * @param {string} [key] - The name of a header to extract.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Headers = createParamDecorator(ParamSource.HEADERS);
diff --git a/src/controller/decorators/params/Param.ts b/src/controller/decorators/params/Param.ts
new file mode 100644
index 0000000..6772e01
--- /dev/null
+++ b/src/controller/decorators/params/Param.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting values from URL path parameters.
+ *
+ * @param {string} key - The name of the path parameter to extract (`/users/{id}`).
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Param = createParamDecorator(ParamSource.PARAM);
diff --git a/src/controller/decorators/params/PathVariable.ts b/src/controller/decorators/params/PathVariable.ts
new file mode 100644
index 0000000..aa967c7
--- /dev/null
+++ b/src/controller/decorators/params/PathVariable.ts
@@ -0,0 +1,3 @@
+import { Param } from "../../../controller/decorators/params";
+
+export const PathVariable = Param;
diff --git a/src/controller/decorators/params/Query.ts b/src/controller/decorators/params/Query.ts
new file mode 100644
index 0000000..223652b
--- /dev/null
+++ b/src/controller/decorators/params/Query.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting values from URL query parameters.
+ *
+ * @param {string} [key] - The name of the query parameter to extract (`?name=value`).
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Query = createParamDecorator(ParamSource.QUERY);
diff --git a/src/controller/decorators/params/Request.ts b/src/controller/decorators/params/Request.ts
new file mode 100644
index 0000000..f92ac2a
--- /dev/null
+++ b/src/controller/decorators/params/Request.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting the request object.
+ *
+ * @param {string} [key] - The name of a key to extract from the request object.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Request = createParamDecorator(ParamSource.REQUEST);
diff --git a/src/controller/decorators/params/RequestBody.ts b/src/controller/decorators/params/RequestBody.ts
new file mode 100644
index 0000000..b750254
--- /dev/null
+++ b/src/controller/decorators/params/RequestBody.ts
@@ -0,0 +1,3 @@
+import { Body } from "../../../controller/decorators/params";
+
+export const RequestBody = Body;
diff --git a/src/controller/decorators/params/RequestParam.ts b/src/controller/decorators/params/RequestParam.ts
new file mode 100644
index 0000000..22287da
--- /dev/null
+++ b/src/controller/decorators/params/RequestParam.ts
@@ -0,0 +1,3 @@
+import { Query } from "../../../controller/decorators/params";
+
+export const RequestParam = Query;
diff --git a/src/controller/decorators/params/Response.ts b/src/controller/decorators/params/Response.ts
new file mode 100644
index 0000000..257f906
--- /dev/null
+++ b/src/controller/decorators/params/Response.ts
@@ -0,0 +1,10 @@
+import { ParamSource } from "../../../domain/enums";
+import { createParamDecorator } from "../../../repository";
+
+/**
+ * A parameter decorator for injecting the response object.
+ *
+ * @param {string} [key] - The name of a key to extract from the response object.
+ * @returns {ParameterDecorator} A parameter decorator.
+ */
+export const Response = createParamDecorator(ParamSource.RESPONSE);
diff --git a/src/controller/decorators/params/index.ts b/src/controller/decorators/params/index.ts
new file mode 100644
index 0000000..ab0c483
--- /dev/null
+++ b/src/controller/decorators/params/index.ts
@@ -0,0 +1,10 @@
+export * from "./Body";
+export * from "./Event";
+export * from "./Headers";
+export * from "./Param";
+export * from "./PathVariable";
+export * from "./Query";
+export * from "./Request";
+export * from "./RequestBody";
+export * from "./RequestParam";
+export * from "./Response";
diff --git a/src/controller/decorators/routing/Delete.ts b/src/controller/decorators/routing/Delete.ts
new file mode 100644
index 0000000..f98aafb
--- /dev/null
+++ b/src/controller/decorators/routing/Delete.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP DELETE requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Delete = createHttpDecorator(RequestMethod.DELETE);
diff --git a/src/controller/decorators/routing/DeleteMapping.ts b/src/controller/decorators/routing/DeleteMapping.ts
new file mode 100644
index 0000000..7ba5668
--- /dev/null
+++ b/src/controller/decorators/routing/DeleteMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP DELETE requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const DeleteMapping = createHttpDecorator(RequestMethod.DELETE);
diff --git a/src/controller/decorators/routing/Get.ts b/src/controller/decorators/routing/Get.ts
new file mode 100644
index 0000000..a51e72a
--- /dev/null
+++ b/src/controller/decorators/routing/Get.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP GET requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Get = createHttpDecorator(RequestMethod.GET);
diff --git a/src/controller/decorators/routing/GetMapping.ts b/src/controller/decorators/routing/GetMapping.ts
new file mode 100644
index 0000000..8917900
--- /dev/null
+++ b/src/controller/decorators/routing/GetMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP GET requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const GetMapping = createHttpDecorator(RequestMethod.GET);
diff --git a/src/controller/decorators/routing/Head.ts b/src/controller/decorators/routing/Head.ts
new file mode 100644
index 0000000..7662b89
--- /dev/null
+++ b/src/controller/decorators/routing/Head.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP HEAD requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Head = createHttpDecorator(RequestMethod.HEAD);
diff --git a/src/controller/decorators/routing/HeadMapping.ts b/src/controller/decorators/routing/HeadMapping.ts
new file mode 100644
index 0000000..0fac6b3
--- /dev/null
+++ b/src/controller/decorators/routing/HeadMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP HEAD requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const HeadMapping = createHttpDecorator(RequestMethod.HEAD);
diff --git a/src/controller/decorators/routing/Options.ts b/src/controller/decorators/routing/Options.ts
new file mode 100644
index 0000000..2792c7f
--- /dev/null
+++ b/src/controller/decorators/routing/Options.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP OPTIONS requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Options = createHttpDecorator(RequestMethod.OPTIONS);
diff --git a/src/controller/decorators/routing/OptionsMapping.ts b/src/controller/decorators/routing/OptionsMapping.ts
new file mode 100644
index 0000000..886fc88
--- /dev/null
+++ b/src/controller/decorators/routing/OptionsMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP OPTIONS requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const OptionsMapping = createHttpDecorator(RequestMethod.OPTIONS);
diff --git a/src/controller/decorators/routing/Patch.ts b/src/controller/decorators/routing/Patch.ts
new file mode 100644
index 0000000..27302b1
--- /dev/null
+++ b/src/controller/decorators/routing/Patch.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP PATCH requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Patch = createHttpDecorator(RequestMethod.PATCH);
diff --git a/src/controller/decorators/routing/PatchMapping.ts b/src/controller/decorators/routing/PatchMapping.ts
new file mode 100644
index 0000000..c011219
--- /dev/null
+++ b/src/controller/decorators/routing/PatchMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP PATCH requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const PatchMapping = createHttpDecorator(RequestMethod.PATCH);
diff --git a/src/controller/decorators/routing/Post.ts b/src/controller/decorators/routing/Post.ts
new file mode 100644
index 0000000..61b5296
--- /dev/null
+++ b/src/controller/decorators/routing/Post.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP POST requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Post = createHttpDecorator(RequestMethod.POST);
diff --git a/src/controller/decorators/routing/PostMapping.ts b/src/controller/decorators/routing/PostMapping.ts
new file mode 100644
index 0000000..2a8ee1a
--- /dev/null
+++ b/src/controller/decorators/routing/PostMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP POST requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const PostMapping = createHttpDecorator(RequestMethod.POST);
diff --git a/src/controller/decorators/routing/Put.ts b/src/controller/decorators/routing/Put.ts
new file mode 100644
index 0000000..e47da52
--- /dev/null
+++ b/src/controller/decorators/routing/Put.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP PUT requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const Put = createHttpDecorator(RequestMethod.PUT);
diff --git a/src/controller/decorators/routing/PutMapping.ts b/src/controller/decorators/routing/PutMapping.ts
new file mode 100644
index 0000000..85e0ffc
--- /dev/null
+++ b/src/controller/decorators/routing/PutMapping.ts
@@ -0,0 +1,10 @@
+import { RequestMethod } from "../../../domain/enums";
+import { createHttpDecorator } from "../../../repository";
+
+/**
+ * Route handler decorator for HTTP PUT requests.
+ *
+ * @param {string} [path] - Route path (optional).
+ * @returns {MethodDecorator} A method decorator.
+ */
+export const PutMapping = createHttpDecorator(RequestMethod.PUT);
diff --git a/src/controller/decorators/routing/index.ts b/src/controller/decorators/routing/index.ts
new file mode 100644
index 0000000..b7e3d3b
--- /dev/null
+++ b/src/controller/decorators/routing/index.ts
@@ -0,0 +1,14 @@
+export * from "./Delete";
+export * from "./DeleteMapping";
+export * from "./Get";
+export * from "./GetMapping";
+export * from "./Head";
+export * from "./HeadMapping";
+export * from "./Options";
+export * from "./OptionsMapping";
+export * from "./Patch";
+export * from "./PatchMapping";
+export * from "./Post";
+export * from "./PostMapping";
+export * from "./Put";
+export * from "./PutMapping";
diff --git a/src/controller/decorators/security/index.ts b/src/controller/decorators/security/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/controller/decorators/validation/index.ts b/src/controller/decorators/validation/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/controller/index.ts b/src/controller/index.ts
new file mode 100644
index 0000000..12eb261
--- /dev/null
+++ b/src/controller/index.ts
@@ -0,0 +1,3 @@
+export * from "./BootApplication";
+export * from "./BootApplicationFactory";
+export * from "./decorators";
diff --git a/src/decorators/class.ts b/src/decorators/class.ts
deleted file mode 100644
index 6ed24c8..0000000
--- a/src/decorators/class.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-import {
- CONTROLLER_OPTIONS_METADATA,
- CONTROLLER_TYPE_METADATA,
- CONTROLLER_WATERMARK,
- INJECTABLE_WATERMARK,
- REPOSITORY_WATERMARK,
- SERVICE_WATERMARK
-} from "../config/constants";
-
-/**
- * A class decorator that marks a class as a generic controller in the application.
- *
- * Controllers serve as entry points for handling various types of requests or events.
- * This decorator does not define the specifics of how to handle an event (e.g., HTTP, Sheets events);
- * that is done using additional, more specific decorators.
- *
- * @returns A class decorator.
- * @see HttpController
- * @see RestController
- * @see DocController
- * @see DocsController
- * @see FormController
- * @see FormsController
- * @see SheetController
- * @see SheetsController
- * @see SlideController
- * @see SlidesController
- * @environment `Google Apps Script`
- */
-export function Controller(
- type?: string | null,
- options?: object | null
-): ClassDecorator {
- return target => {
- Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
-
- if (type) {
- Reflect.defineMetadata(CONTROLLER_TYPE_METADATA, type, target);
- }
-
- if (options) {
- Reflect.defineMetadata(CONTROLLER_OPTIONS_METADATA, options, target);
- }
- };
-}
-
-/**
- * A class decorator that marks a class as a service.
- *
- * Services typically contain the application's business logic and interact with repositories. Classes marked with `@Service` can be automatically injected into other components (e.g., controllers) using a dependency injection system.
- *
- * @returns A class decorator.
- * @see Repository
- * @see Injectable
- * @environment `Google Apps Script`
- */
-export function Service(): ClassDecorator {
- return (target: object) => {
- Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
- Reflect.defineMetadata(SERVICE_WATERMARK, true, target);
- };
-}
-
-/**
- * A class decorator that marks a class as a repository.
- *
- * Repositories are responsible for abstracting data access logic (e.g., interacting with a database, external APIs, or, in the case of Google Apps Script, with Google Sheets, Docs, etc.).
- *
- * @returns A class decorator.
- * @see Service
- * @see Injectable
- * @environment `Google Apps Script`
- */
-export function Repository(): ClassDecorator {
- return (target: object) => {
- Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
- Reflect.defineMetadata(REPOSITORY_WATERMARK, true, target);
- };
-}
-
-/**
- * A class decorator that indicates the class can be injected by a dependency injection container.
- *
- * This is a universal decorator used to register classes in the DI container, making them available for injection into other components.
- * It can be used for classes that do not fall under the `@Service` or `@Repository` categories but still need to be managed by DI (e.g., utility classes, configuration classes).
- *
- * @returns A class decorator.
- * @see Service
- * @see Repository
- * @environment `Google Apps Script`
- */
-export function Injectable(): ClassDecorator {
- return (target: object) => {
- Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
- };
-}
-
-/**
- * A class decorator that marks a class as a controller capable of handling incoming requests.
- *
- * Controllers are responsible for routing requests to the corresponding handler methods.
- *
- * @param [basePath='/'] - The base URL path for all routes defined in this controller's methods.
- * @returns A class decorator.
- * @see Controller
- * @see RestController
- * @environment `Google Apps Script`
- */
-export function HttpController(
- basePath: string | undefined = "/"
-): ClassDecorator {
- return target => {
- Controller("http", {
- basePath
- })(target);
- };
-}
-
-/**
- * A class decorator equivalent to {@link HttpController}.
- */
-export const RestController = HttpController;
-
-/**
- * A class decorator that marks a class as a controller intended to handle
- * Google Docs events (onOpen, etc.).
- *
- * @returns A class decorator.
- * @see Controller
- * @see DocsController
- * @see FormController
- * @see FormsController
- * @see SheetController
- * @see SheetsController
- * @see SlideController
- * @see SlidesController
- * @environment `Google Apps Script`
- */
-export function DocController(): ClassDecorator {
- return target => {
- Controller("appsscript:doc")(target);
- };
-}
-
-/**
- * A class decorator equivalent to {@link DocController}.
- */
-export const DocsController = DocController;
-
-/**
- * A class decorator that marks a class as a controller intended to handle
- * Google Forms events (onOpen, etc.).
- *
- * @returns A class decorator.
- * @see Controller
- * @see DocController
- * @see DocsController
- * @see FormsController
- * @see SheetController
- * @see SheetsController
- * @see SlideController
- * @see SlidesController
- * @environment `Google Apps Script`
- */
-export function FormController(): ClassDecorator {
- return target => {
- Controller("appsscript:form")(target);
- };
-}
-
-/**
- * A class decorator equivalent to {@link FormController}.
- */
-export const FormsController = FormController;
-
-/**
- * A class decorator that marks a class as a controller intended to handle
- * Google Sheets events (onOpen, onEdit, onChange, etc.).
- *
- * @param [sheetName] - An optional sheet name (or names/RegExp) to which this controller applies. If not specified, the controller can handle events for any sheet unless overridden at the method level.
- *
- * @returns A class decorator.
- * @see Controller
- * @see DocController
- * @see DocsController
- * @see FormController
- * @see FormsController
- * @see SheetsController
- * @see SlideController
- * @see SlidesController
- * @environment `Google Apps Script`
- */
-export function SheetController(
- sheetName?: string | string[] | RegExp
-): ClassDecorator {
- return target => {
- Controller("appsscript:sheet", { sheetName })(target);
- };
-}
-
-/**
- * A class decorator equivalent to {@link SheetController}.
- */
-export const SheetsController = SheetController;
-
-/**
- * A class decorator that marks a class as a controller intended to handle
- * Google Slides events (onOpen, etc.).
- *
- * @returns A class decorator.
- * @see Controller
- * @see DocController
- * @see DocsController
- * @see FormController
- * @see FormsController
- * @see SheetController
- * @see SheetsController
- * @see SlidesController
- * @environment `Google Apps Script`
- */
-export function SlideController(): ClassDecorator {
- return target => {
- Controller("appsscript:slide")(target);
- };
-}
-
-/**
- * A class decorator equivalent to {@link SlideController}.
- */
-export const SlidesController = SlideController;
diff --git a/src/decorators/index.ts b/src/decorators/index.ts
deleted file mode 100644
index ec3e187..0000000
--- a/src/decorators/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from "./class";
-export * from "./method";
-export * from "./param";
diff --git a/src/decorators/method.ts b/src/decorators/method.ts
deleted file mode 100644
index 50b8612..0000000
--- a/src/decorators/method.ts
+++ /dev/null
@@ -1,336 +0,0 @@
-import { isRegExp, isString, nonEmpty } from "apps-script-utils";
-import { AppsScriptEventType, RequestMethod } from "../types";
-import {
- AppsScriptOptions,
- createHttpDecorator,
- createMethodDecorator
-} from "../utils";
-
-/**
- * A method decorator for handling the `onInstall` event in Google Sheets.
- *
- * It fires when the add-on is first installed by a user.
- *
- * @returns A method decorator.
- * @see Open
- * @see Edit
- * @see Change
- * @see SelectionChange
- * @see FormSubmit
- * @environment `Google Apps Script`
- */
-export const Install = () => createMethodDecorator(AppsScriptEventType.INSTALL);
-
-/**
- * A method decorator for handling the `onOpen` event in Google Sheets.
- *
- * It fires when a user opens the spreadsheet.
- *
- * @returns A method decorator.
- * @see Install
- * @see Edit
- * @see Change
- * @see SelectionChange
- * @see FormSubmit
- * @environment `Google Apps Script`
- */
-export const Open = () => createMethodDecorator(AppsScriptEventType.OPEN);
-
-/**
- * A method decorator for handling the `onEdit` event in Google Sheets.
- *
- * It fires when the contents of a spreadsheet's cells are changed manually.
- *
- * @param args - An optional list of one or more A1-notations (e.g., 'A1:C5'), sheet names (e.g., 'Sheet1'), or a regular expression.
- * @returns A method decorator.
- * @see Install
- * @see Open
- * @see Change
- * @see SelectionChange
- * @see FormSubmit
- * @environment `Google Apps Script`
- */
-export const Edit = (...args: (string | RegExp | string[])[]) => {
- const options: Partial> = {};
-
- if (args.length > 0) {
- if (isRegExp(args[0]) && args.length === 1) {
- options.range = args[0];
- } else {
- const flattenedRanges = args.flat(Infinity).filter(isString) as string[];
-
- if (nonEmpty(flattenedRanges)) {
- options.range =
- flattenedRanges.length === 1 ? flattenedRanges[0] : flattenedRanges;
- } else if (
- args.some(arg => isRegExp(arg) || Array.isArray(arg) || isString(arg))
- ) {
- console.warn("Edit decorator: Unsupported or invalid range argument.");
- }
- }
- }
-
- return createMethodDecorator(AppsScriptEventType.EDIT, options);
-};
-
-/**
- * A method decorator for handling the `onChange` event in Google Sheets.
- *
- * It fires on any change in the spreadsheet (including changes to formulas, moving rows, inserting images, etc.).
- *
- * @param [changeType] - An optional filter by change type.
- * @returns A method decorator.
- * @see Install
- * @see Open
- * @see Edit
- * @see SelectionChange
- * @see FormSubmit
- * @environment `Google Apps Script`
- */
-export const Change = (
- changeType?:
- | GoogleAppsScript.Events.SheetsOnChangeChangeType
- | GoogleAppsScript.Events.SheetsOnChangeChangeType[]
-) => createMethodDecorator(AppsScriptEventType.CHANGE, { changeType });
-
-/**
- * A method decorator for handling the `onSelectionChange` event in Google Sheets.
- *
- * It fires when a user's cell selection changes.
- *
- * @returns A method decorator.
- * @see Install
- * @see Open
- * @see Edit
- * @see Change
- * @see FormSubmit
- * @environment `Google Apps Script`
- */
-export const SelectionChange = () =>
- createMethodDecorator(AppsScriptEventType.SELECTION_CHANGE);
-
-/**
- * A method decorator for handling the `onFormSubmit` event in Google Sheets.
- *
- * It fires when a form is submitted and its data is recorded in the spreadsheet.
- *
- * @param args
- * @returns A method decorator.
- * @see Install
- * @see Open
- * @see Edit
- * @see Change
- * @see SelectionChange
- * @environment `Google Apps Script`
- */
-export const FormSubmit = (...args: (string | string[])[]) => {
- const options: Partial> = {};
-
- if (args.length > 0) {
- const flattenedFormIds = args
- .flat(Infinity)
- .filter(item => typeof item === "string") as string[];
-
- if (flattenedFormIds.length > 0) {
- options.formId =
- flattenedFormIds.length === 1 ? flattenedFormIds[0] : flattenedFormIds;
- } else if (
- args.some(arg => typeof arg === "string" || Array.isArray(arg))
- ) {
- console.warn(
- "FormSubmit decorator: Unsupported or invalid form ID argument."
- );
- }
- }
-
- return createMethodDecorator(AppsScriptEventType.FORM_SUBMIT, options);
-};
-
-/**
- * A method decorator for handling HTTP POST requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Post = createHttpDecorator(RequestMethod.POST);
-
-/**
- * A method decorator for handling HTTP GET requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Get = createHttpDecorator(RequestMethod.GET);
-
-/**
- * A method decorator for handling HTTP DELETE requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Delete = createHttpDecorator(RequestMethod.DELETE);
-
-/**
- * A method decorator for handling HTTP PUT requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Put = createHttpDecorator(RequestMethod.PUT);
-
-/**
- * A method decorator for handling HTTP PATCH requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Patch = createHttpDecorator(RequestMethod.PATCH);
-
-/**
- * A method decorator for handling HTTP OPTIONS requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see OptionsMapping
- * @see Head
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Options = createHttpDecorator(RequestMethod.OPTIONS);
-
-/**
- * A method decorator for handling HTTP HEAD requests.
- *
- * @param [path='/'] - The relative path for the route.
- * @returns A method decorator.
- * @see Post
- * @see PostMapping
- * @see Get
- * @see GetMapping
- * @see Delete
- * @see DeleteMapping
- * @see Put
- * @see PutMapping
- * @see Patch
- * @see PatchMapping
- * @see Options
- * @see OptionsMapping
- * @see HeadMapping
- * @environment `Google Apps Script`
- */
-export const Head = createHttpDecorator(RequestMethod.HEAD);
-
-/**
- * A method decorator equivalent to {@link Post}.
- */
-export const PostMapping = createHttpDecorator(RequestMethod.POST);
-
-/**
- * A method decorator equivalent to {@link Get}.
- */
-export const GetMapping = createHttpDecorator(RequestMethod.GET);
-
-/**
- * A method decorator equivalent to {@link Delete}.
- */
-export const DeleteMapping = createHttpDecorator(RequestMethod.DELETE);
-
-/**
- * A method decorator equivalent to {@link Put}.
- */
-export const PutMapping = createHttpDecorator(RequestMethod.PUT);
-
-/**
- * A method decorator equivalent to {@link Patch}.
- */
-export const PatchMapping = createHttpDecorator(RequestMethod.PATCH);
-
-/**
- * A method decorator equivalent to {@link Options}.
- */
-export const OptionsMapping = createHttpDecorator(RequestMethod.OPTIONS);
-
-/**
- * A method decorator equivalent to {@link Head}.
- */
-export const HeadMapping = createHttpDecorator(RequestMethod.HEAD);
diff --git a/src/decorators/param.ts b/src/decorators/param.ts
deleted file mode 100644
index a4c07dd..0000000
--- a/src/decorators/param.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import { INJECT_TOKENS_METADATA } from "../config/constants";
-import { InjectTokenDefinition, Newable, ParamSource } from "../types";
-import { assignInjectMetadata, createParamDecorator } from "../utils";
-
-/**
- * A parameter decorator for injecting values from URL path parameters.
- * This is a generic decorator for path parameters.
- *
- * @param key - The name of the path parameter to extract (`/users/{id}`).
- * @returns A parameter decorator.
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Param = createParamDecorator(ParamSource.PARAM);
-
-/**
- * A parameter decorator for injecting values from URL query parameters.
- * This is a generic decorator for query parameters.
- *
- * @param [key] - The name of the query parameter to extract (`?name=value`).
- * @returns A parameter decorator.
- * @see Param
- * @see PathVariable
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Query = createParamDecorator(ParamSource.QUERY);
-
-/**
- * A parameter decorator for injecting the full request body.
- * It is typically used for HTTP POST/PUT/PATCH requests.
- *
- * @param [key] - The name of a key to extract a specific value from the request body (e.g., 'name' from JSON: `{ "name": "value" }`).
- * If not specified, the full request body is injected.
- * @see Param
- * @returns A parameter decorator.
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Body = createParamDecorator(ParamSource.BODY);
-
-/**
- * A parameter decorator for injecting the request object.
- *
- * @param [key] - The name of a key to extract a specific value from the request object. If not specified, the entire request object is injected.
- * @returns A parameter decorator.
- * @see Param
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Headers
- * @see Response
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Request = createParamDecorator(ParamSource.REQUEST);
-
-/**
- * A parameter decorator for injecting request headers.
- *
- * @param [key] - The name of a header key to extract a specific value. If not specified, all request headers are injected as an object.
- * @returns A parameter decorator.
- * @see Param
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Headers = createParamDecorator(ParamSource.HEADERS);
-
-/**
- * A parameter decorator for injecting the response object.
- *
- * It is used to get a reference to the response object to set headers, status codes, or modify the response before it is sent.
- *
- * @returns A parameter decorator.
- * @see Param
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Event
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Response = createParamDecorator(ParamSource.RESPONSE);
-
-/**
- * A parameter decorator equivalent to {@link Param}.
- */
-export const PathVariable = createParamDecorator(ParamSource.PARAM);
-
-/**
- * A parameter decorator equivalent to {@link Query}.
- */
-export const RequestParam = createParamDecorator(ParamSource.QUERY);
-
-/**
- * A parameter decorator equivalent to {@link Body}.
- */
-export const RequestBody = createParamDecorator(ParamSource.BODY);
-
-/**
- * A parameter decorator used to inject the full Google Apps Script event object.
- *
- * @returns A parameter decorator.
- * @see Path
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Inject
- * @environment `Google Apps Script`
- */
-export const Event = createParamDecorator(ParamSource.EVENT);
-
-/**
- * A parameter decorator used to explicitly specify an injection token for a dependency.
- *
- * This is useful when a parameter's type cannot be determined by reflection (e.g., when using interfaces), or when you need to inject a specific implementation that is different from the type.
- *
- * @param [token] - The injection token that the DI container will use to resolve the dependency. This is typically a class constructor (Constructor), but can also be a Symbol, string, or any other unique identifier.
- * @returns A parameter decorator.
- * @see Path
- * @see PathVariable
- * @see Query
- * @see RequestParam
- * @see Body
- * @see RequestBody
- * @see Request
- * @see Headers
- * @see Response
- * @see Event
- * @environment `Google Apps Script`
- */
-export function Inject(token?: Newable | string | symbol): ParameterDecorator {
- return (target, propertyKey, parameterIndex) => {
- const metadataTarget =
- typeof target === "function" ? target : target.constructor;
-
- const existing: Record =
- (propertyKey
- ? Reflect.getMetadata(
- INJECT_TOKENS_METADATA,
- metadataTarget,
- propertyKey
- )
- : Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget)) || {};
-
- const updatedTokens = assignInjectMetadata(existing, parameterIndex, token);
-
- if (propertyKey) {
- Reflect.defineMetadata(
- INJECT_TOKENS_METADATA,
- updatedTokens,
- metadataTarget,
- propertyKey
- );
- } else {
- Reflect.defineMetadata(
- INJECT_TOKENS_METADATA,
- updatedTokens,
- metadataTarget
- );
- }
- };
-}
diff --git a/src/domain/constants/appsscript.ts b/src/domain/constants/appsscript.ts
new file mode 100644
index 0000000..8e7bdb5
--- /dev/null
+++ b/src/domain/constants/appsscript.ts
@@ -0,0 +1,3 @@
+export const APPSSCRIPT_EVENT_METADATA = "appsscript:event";
+
+export const APPSSCRIPT_OPTIONS_METADATA = "appsscript:options";
diff --git a/src/domain/constants/http.ts b/src/domain/constants/http.ts
new file mode 100644
index 0000000..3e5b0f2
--- /dev/null
+++ b/src/domain/constants/http.ts
@@ -0,0 +1,3 @@
+export const METHOD_METADATA = "http:method";
+
+export const PATH_METADATA = "http:path";
diff --git a/src/domain/constants/index.ts b/src/domain/constants/index.ts
new file mode 100644
index 0000000..f4596f7
--- /dev/null
+++ b/src/domain/constants/index.ts
@@ -0,0 +1,3 @@
+export * from "./appsscript";
+export * from "./http";
+export * from "./metadata";
diff --git a/src/config/constants.ts b/src/domain/constants/metadata.ts
similarity index 68%
rename from src/config/constants.ts
rename to src/domain/constants/metadata.ts
index a76c732..3fa2284 100644
--- a/src/config/constants.ts
+++ b/src/domain/constants/metadata.ts
@@ -1,19 +1,12 @@
export const CONTROLLER_WATERMARK = "__controller__";
export const SERVICE_WATERMARK = "__service__";
export const REPOSITORY_WATERMARK = "__repository__";
+export const ENTITY_WATERMARK = "__entity__";
export const INJECTABLE_WATERMARK = "__injectable__";
export const CONTROLLER_TYPE_METADATA = "controller:type";
export const CONTROLLER_OPTIONS_METADATA = "controller:options";
-// http
-export const METHOD_METADATA = "http:method";
-export const PATH_METADATA = "http:path";
-
-// appsscript
-export const APPSSCRIPT_EVENT_METADATA = "appsscript:event";
-export const APPSSCRIPT_OPTIONS_METADATA = "appsscript:options";
-
export const INJECT_TOKENS_METADATA = "custom:inject";
export const PARAM_DEFINITIONS_METADATA = "custom:param";
diff --git a/src/domain/entities/RouteExecutionContext.ts b/src/domain/entities/RouteExecutionContext.ts
new file mode 100644
index 0000000..5b5df41
--- /dev/null
+++ b/src/domain/entities/RouteExecutionContext.ts
@@ -0,0 +1,14 @@
+import { HttpHeaders, HttpRequest, HttpResponse, ParsedUrlQuery } from "../../domain/types";
+
+/**
+ * Execution context for a route handler.
+ */
+export interface RouteExecutionContext {
+ event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost;
+ params: Record;
+ query: ParsedUrlQuery;
+ request: HttpRequest;
+ headers: HttpHeaders;
+ body: unknown;
+ response: HttpResponse;
+}
diff --git a/src/domain/entities/index.ts b/src/domain/entities/index.ts
new file mode 100644
index 0000000..210a9d0
--- /dev/null
+++ b/src/domain/entities/index.ts
@@ -0,0 +1 @@
+export * from "./RouteExecutionContext";
diff --git a/src/domain/enums/AppsScriptEventType.ts b/src/domain/enums/AppsScriptEventType.ts
new file mode 100644
index 0000000..dd55884
--- /dev/null
+++ b/src/domain/enums/AppsScriptEventType.ts
@@ -0,0 +1,48 @@
+/**
+ * Перечисление типов событий Google Apps Script.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers
+ */
+export enum AppsScriptEventType {
+ /**
+ * Срабатывает при установке дополнения.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers#oninstall
+ */
+ INSTALL = "INSTALL",
+
+ /**
+ * Срабатывает при открытии документа, таблицы, презентации или формы.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers#onopen
+ */
+ OPEN = "OPEN",
+
+ /**
+ * Срабатывает, когда пользователь изменяет значение ячейки в таблице.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers#onedit
+ */
+ EDIT = "EDIT",
+
+ /**
+ * Срабатывает, когда пользователь изменяет структуру таблицы (например, добавляет строку).
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers/installable#change
+ */
+ CHANGE = "CHANGE",
+
+ /**
+ * Срабатывает, когда пользователь меняет выделение в таблице.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers#onselectionchange
+ */
+ SELECTION_CHANGE = "SELECTION_CHANGE",
+
+ /**
+ * Срабатывает, когда пользователь отправляет форму или отвечает на опрос.
+ *
+ * @see https://developers.google.com/apps-script/guides/triggers/installable#form-submit
+ */
+ FORM_SUBMIT = "FORM_SUBMIT"
+}
diff --git a/src/domain/enums/HeaderAcceptMimeType.ts b/src/domain/enums/HeaderAcceptMimeType.ts
new file mode 100644
index 0000000..4079c7d
--- /dev/null
+++ b/src/domain/enums/HeaderAcceptMimeType.ts
@@ -0,0 +1,39 @@
+/**
+ * Перечисление MIME-типов, используемых в заголовке Accept для определения формата ответа.
+ *
+ * @see https://developers.google.com/apps-script/reference/content/mime-type
+ */
+export enum HeaderAcceptMimeType {
+ /**
+ * Специальный тип для возврата JSON-строки напрямую (без TextOutput).
+ * Используется для внутренних нужд Google Apps Script.
+ */
+ GOOGLE_TEXT = "google/plain",
+
+ /**
+ * Специальный тип для возврата JSON-строки напрямую (без TextOutput).
+ * Используется для внутренних нужд Google Apps Script.
+ */
+ GOOGLE_JSON = "google/json",
+
+ /**
+ * HTML контент.
+ *
+ * @see https://developers.google.com/apps-script/reference/html/html-service
+ */
+ HTML = "text/html",
+
+ /**
+ * Простой текст.
+ *
+ * @see https://developers.google.com/apps-script/reference/content/mime-type
+ */
+ TEXT = "text/plain",
+
+ /**
+ * Данные в формате JSON.
+ *
+ * @see https://developers.google.com/apps-script/reference/content/mime-type
+ */
+ JSON = "application/json"
+}
diff --git a/src/types/HttpStatus.ts b/src/domain/enums/HttpStatus.ts
similarity index 100%
rename from src/types/HttpStatus.ts
rename to src/domain/enums/HttpStatus.ts
diff --git a/src/types/ParamSource.ts b/src/domain/enums/ParamSource.ts
similarity index 100%
rename from src/types/ParamSource.ts
rename to src/domain/enums/ParamSource.ts
index 6f81ba0..1c9e0a4 100644
--- a/src/types/ParamSource.ts
+++ b/src/domain/enums/ParamSource.ts
@@ -2,9 +2,9 @@ export enum ParamSource {
PARAM = "PARAM",
QUERY = "QUERY",
BODY = "BODY",
- EVENT = "EVENT",
- REQUEST = "REQUEST",
HEADERS = "HEADERS",
+ REQUEST = "REQUEST",
RESPONSE = "RESPONSE",
+ EVENT = "EVENT",
INJECT = "INJECT"
}
diff --git a/src/types/RequestMethod.ts b/src/domain/enums/RequestMethod.ts
similarity index 100%
rename from src/types/RequestMethod.ts
rename to src/domain/enums/RequestMethod.ts
diff --git a/src/domain/enums/index.ts b/src/domain/enums/index.ts
new file mode 100644
index 0000000..f3ffb85
--- /dev/null
+++ b/src/domain/enums/index.ts
@@ -0,0 +1,5 @@
+export * from "./AppsScriptEventType";
+export * from "./HeaderAcceptMimeType";
+export * from "./HttpStatus";
+export * from "./ParamSource";
+export * from "./RequestMethod";
diff --git a/src/domain/index.ts b/src/domain/index.ts
new file mode 100644
index 0000000..51b1446
--- /dev/null
+++ b/src/domain/index.ts
@@ -0,0 +1,4 @@
+export * from "./constants";
+export * from "./entities";
+export * from "./enums";
+export * from "./types";
diff --git a/src/types/AppConfig.ts b/src/domain/types/ApplicationConfig.ts
similarity index 78%
rename from src/types/AppConfig.ts
rename to src/domain/types/ApplicationConfig.ts
index ac428ca..19318e8 100644
--- a/src/types/AppConfig.ts
+++ b/src/domain/types/ApplicationConfig.ts
@@ -1,7 +1,7 @@
import { Newable } from "./Newable";
import { Provider } from "./Provider";
-export interface AppConfig {
+export interface ApplicationConfig {
controllers?: Newable[];
providers?: Provider[];
}
diff --git a/src/domain/types/ClassProvider.ts b/src/domain/types/ClassProvider.ts
new file mode 100644
index 0000000..98eadb3
--- /dev/null
+++ b/src/domain/types/ClassProvider.ts
@@ -0,0 +1,7 @@
+import { InjectionToken } from "./InjectionToken";
+import { Newable } from "./Newable";
+
+export interface ClassProvider {
+ provide: InjectionToken;
+ useClass: Newable;
+}
diff --git a/src/types/ErrorResponse.ts b/src/domain/types/ErrorResponse.ts
similarity index 100%
rename from src/types/ErrorResponse.ts
rename to src/domain/types/ErrorResponse.ts
diff --git a/src/domain/types/ExistingProvider.ts b/src/domain/types/ExistingProvider.ts
new file mode 100644
index 0000000..9d607aa
--- /dev/null
+++ b/src/domain/types/ExistingProvider.ts
@@ -0,0 +1,6 @@
+import { InjectionToken } from "./InjectionToken";
+
+export interface ExistingProvider {
+ provide: InjectionToken;
+ useExisting: InjectionToken;
+}
diff --git a/src/domain/types/FactoryProvider.ts b/src/domain/types/FactoryProvider.ts
new file mode 100644
index 0000000..cad9bdb
--- /dev/null
+++ b/src/domain/types/FactoryProvider.ts
@@ -0,0 +1,7 @@
+import { InjectionToken } from "./InjectionToken";
+
+export interface FactoryProvider {
+ provide: InjectionToken;
+ useFactory: (...args: unknown[]) => T;
+ inject?: InjectionToken[];
+}
diff --git a/src/types/HttpHeaders.ts b/src/domain/types/HttpHeaders.ts
similarity index 100%
rename from src/types/HttpHeaders.ts
rename to src/domain/types/HttpHeaders.ts
diff --git a/src/types/HttpRequest.ts b/src/domain/types/HttpRequest.ts
similarity index 79%
rename from src/types/HttpRequest.ts
rename to src/domain/types/HttpRequest.ts
index 924e7fc..6f2f4eb 100644
--- a/src/types/HttpRequest.ts
+++ b/src/domain/types/HttpRequest.ts
@@ -1,6 +1,6 @@
import { HttpHeaders } from "./HttpHeaders";
import { ParsedUrl } from "./ParsedUrl";
-import { RequestMethod } from "./RequestMethod";
+import { RequestMethod } from "../../domain/enums";
export interface HttpRequest {
headers: HttpHeaders;
diff --git a/src/types/HttpResponse.ts b/src/domain/types/HttpResponse.ts
similarity index 78%
rename from src/types/HttpResponse.ts
rename to src/domain/types/HttpResponse.ts
index 276c6c6..c85ffa1 100644
--- a/src/types/HttpResponse.ts
+++ b/src/domain/types/HttpResponse.ts
@@ -1,5 +1,5 @@
import { HttpHeaders } from "./HttpHeaders";
-import { HttpStatus } from "./HttpStatus";
+import { HttpStatus } from "../../domain/enums";
export interface HttpResponse {
headers: HttpHeaders;
diff --git a/src/domain/types/InjectTokenDefinition.ts b/src/domain/types/InjectTokenDefinition.ts
new file mode 100644
index 0000000..9381e1c
--- /dev/null
+++ b/src/domain/types/InjectTokenDefinition.ts
@@ -0,0 +1,8 @@
+import { ParamSource } from "../../domain/enums";
+import { InjectionToken } from "./InjectionToken";
+
+export interface InjectTokenDefinition {
+ type: ParamSource.INJECT;
+ token?: InjectionToken;
+ index: number;
+}
diff --git a/src/domain/types/InjectionToken.ts b/src/domain/types/InjectionToken.ts
new file mode 100644
index 0000000..abb25df
--- /dev/null
+++ b/src/domain/types/InjectionToken.ts
@@ -0,0 +1,6 @@
+import { Newable } from "./Newable";
+
+/**
+ * Represents an injection token.
+ */
+export type InjectionToken = string | symbol | Newable;
diff --git a/src/domain/types/Newable.ts b/src/domain/types/Newable.ts
new file mode 100644
index 0000000..7dda003
--- /dev/null
+++ b/src/domain/types/Newable.ts
@@ -0,0 +1 @@
+export type Newable = new (...args: never[]) => T;
diff --git a/src/domain/types/ParamDefinition.ts b/src/domain/types/ParamDefinition.ts
new file mode 100644
index 0000000..abb96e1
--- /dev/null
+++ b/src/domain/types/ParamDefinition.ts
@@ -0,0 +1,7 @@
+import { ParamSource } from "../../domain/enums";
+
+export interface ParamDefinition {
+ type: ParamSource;
+ key?: string;
+ index: number;
+}
diff --git a/src/domain/types/ParsedUrl.ts b/src/domain/types/ParsedUrl.ts
new file mode 100644
index 0000000..84d3ee2
--- /dev/null
+++ b/src/domain/types/ParsedUrl.ts
@@ -0,0 +1,8 @@
+import { ParsedUrlQuery } from "./ParsedUrlQuery";
+
+export interface ParsedUrl {
+ pathname: string;
+ path: string;
+ search?: string;
+ query: ParsedUrlQuery;
+}
diff --git a/src/types/ParsedUrlQuery.ts b/src/domain/types/ParsedUrlQuery.ts
similarity index 100%
rename from src/types/ParsedUrlQuery.ts
rename to src/domain/types/ParsedUrlQuery.ts
diff --git a/src/domain/types/Provider.ts b/src/domain/types/Provider.ts
new file mode 100644
index 0000000..6f830a0
--- /dev/null
+++ b/src/domain/types/Provider.ts
@@ -0,0 +1,12 @@
+import { ClassProvider } from "./ClassProvider";
+import { ExistingProvider } from "./ExistingProvider";
+import { FactoryProvider } from "./FactoryProvider";
+import { Newable } from "./Newable";
+import { ValueProvider } from "./ValueProvider";
+
+export type Provider =
+ | Newable
+ | ValueProvider
+ | ClassProvider
+ | FactoryProvider
+ | ExistingProvider;
diff --git a/src/types/RouteMetadata.ts b/src/domain/types/RouteMetadata.ts
similarity index 76%
rename from src/types/RouteMetadata.ts
rename to src/domain/types/RouteMetadata.ts
index e7e1d79..7bc12c7 100644
--- a/src/types/RouteMetadata.ts
+++ b/src/domain/types/RouteMetadata.ts
@@ -1,9 +1,9 @@
import { Newable } from "./Newable";
-import { RequestMethod } from "./RequestMethod";
+import { RequestMethod } from "../../domain/enums";
export interface RouteMetadata {
+ controller: Newable;
+ handler: string | symbol;
method: RequestMethod;
path: string;
- handler: string | symbol;
- controller: Newable;
}
diff --git a/src/domain/types/ValueProvider.ts b/src/domain/types/ValueProvider.ts
new file mode 100644
index 0000000..5df70aa
--- /dev/null
+++ b/src/domain/types/ValueProvider.ts
@@ -0,0 +1,6 @@
+import { InjectionToken } from "./InjectionToken";
+
+export interface ValueProvider {
+ provide: InjectionToken;
+ useValue: T;
+}
diff --git a/src/domain/types/index.ts b/src/domain/types/index.ts
new file mode 100644
index 0000000..73782f3
--- /dev/null
+++ b/src/domain/types/index.ts
@@ -0,0 +1,17 @@
+export * from "./ApplicationConfig";
+export * from "./ClassProvider";
+export * from "./ErrorResponse";
+export * from "./ExistingProvider";
+export * from "./FactoryProvider";
+export * from "./HttpHeaders";
+export * from "./HttpRequest";
+export * from "./HttpResponse";
+export * from "./InjectTokenDefinition";
+export * from "./InjectionToken";
+export * from "./Newable";
+export * from "./ParamDefinition";
+export * from "./ParsedUrl";
+export * from "./ParsedUrlQuery";
+export * from "./Provider";
+export * from "./RouteMetadata";
+export * from "./ValueProvider";
diff --git a/src/exceptions/AppException.ts b/src/exceptions/AppException.ts
new file mode 100644
index 0000000..ff3d492
--- /dev/null
+++ b/src/exceptions/AppException.ts
@@ -0,0 +1,15 @@
+/**
+ * Base exception class for all exceptions in the application.
+ *
+ * @param {string} message - The error message.
+ * @param {number} [status] - The HTTP status code.
+ */
+export class AppException extends Error {
+ constructor(
+ public readonly message: string,
+ public readonly status: number = 500
+ ) {
+ super(message);
+ this.name = "AppException";
+ }
+}
diff --git a/src/exceptions/HttpException.ts b/src/exceptions/HttpException.ts
new file mode 100644
index 0000000..3c74481
--- /dev/null
+++ b/src/exceptions/HttpException.ts
@@ -0,0 +1,12 @@
+import { HttpStatus } from "../domain/enums";
+import { AppException } from "../exceptions";
+
+export class HttpException extends AppException {
+ constructor(
+ public readonly message: string,
+ public readonly status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR
+ ) {
+ super(message, status);
+ this.name = "HttpException";
+ }
+}
diff --git a/src/exceptions/README.md b/src/exceptions/README.md
new file mode 100644
index 0000000..abad06e
--- /dev/null
+++ b/src/exceptions/README.md
@@ -0,0 +1,7 @@
+# Exceptions Layer
+
+Этот слой содержит классы ошибок и механизмы их обработки.
+
+- `app.exception.ts`: Базовый класс для исключений приложения.
+- `http.exception.ts`: Исключения, специфичные для HTTP.
+- `decorators/`: Декораторы для обработки ошибок (например, `@Catch`).
diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts
new file mode 100644
index 0000000..0ec57e6
--- /dev/null
+++ b/src/exceptions/index.ts
@@ -0,0 +1,2 @@
+export * from "./AppException";
+export * from "./HttpException";
diff --git a/src/index.ts b/src/index.ts
index 5f23af7..5ae5c4a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,9 @@
-import "reflect-metadata";
-import { App } from "./App";
+import { BootApplication, BootApplicationFactory } from "./controller";
-export * from "./types";
-export * from "./decorators";
+export * from "./controller";
+export * from "./utils";
+export * from "./domain/types";
+export * from "./domain/enums";
-export { App };
-export const createApp = App.create;
+export { BootApplication as App };
+export const createApp = BootApplicationFactory.create;
diff --git a/src/repository/MetadataRepository.ts b/src/repository/MetadataRepository.ts
new file mode 100644
index 0000000..082d21d
--- /dev/null
+++ b/src/repository/MetadataRepository.ts
@@ -0,0 +1,71 @@
+/**
+ * Repository for managing metadata across the framework.
+ */
+export class MetadataRepository {
+ /**
+ * Retrieves metadata for a given key from a target object.
+ *
+ * @param {unknown} metadataKey - The metadata key.
+ * @param {object} target - The target object.
+ * @param {string | symbol} [propertyKey] - The optional property key.
+ * @returns {T | undefined} The metadata value.
+ */
+ public getMetadata(
+ metadataKey: unknown,
+ target: object,
+ propertyKey?: string | symbol
+ ): T | undefined {
+ return propertyKey
+ ? Reflect.getMetadata(metadataKey, target, propertyKey)
+ : Reflect.getMetadata(metadataKey, target);
+ }
+
+ /**
+ * Defines metadata for a given key on a target object.
+ *
+ * @param {unknown} metadataKey - The metadata key.
+ * @param {unknown} metadataValue - The metadata value.
+ * @param {object} target - The target object.
+ * @param {string | symbol} [propertyKey] - The optional property key.
+ * @returns {void}
+ */
+ public defineMetadata(
+ metadataKey: unknown,
+ metadataValue: unknown,
+ target: object,
+ propertyKey?: string | symbol
+ ): void {
+ if (propertyKey) {
+ Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
+ } else {
+ Reflect.defineMetadata(metadataKey, metadataValue, target);
+ }
+ }
+
+ /**
+ * Checks if metadata for a given key exists on a target object.
+ *
+ * @param {unknown} metadataKey - The metadata key.
+ * @param {object} target - The target object.
+ * @param {string | symbol} [propertyKey] - The optional property key.
+ * @returns {boolean} True if metadata exists, false otherwise.
+ */
+ public hasMetadata(metadataKey: unknown, target: object, propertyKey?: string | symbol): boolean {
+ return propertyKey
+ ? Reflect.hasMetadata(metadataKey, target, propertyKey)
+ : Reflect.hasMetadata(metadataKey, target);
+ }
+
+ /**
+ * Retrieves all own metadata keys from a target object.
+ *
+ * @param {object} target - The target object.
+ * @param {string | symbol} [propertyKey] - The optional property key.
+ * @returns {unknown[]} An array of metadata keys.
+ */
+ public getOwnMetadataKeys(target: object, propertyKey?: string | symbol): unknown[] {
+ return propertyKey
+ ? Reflect.getOwnMetadataKeys(target, propertyKey)
+ : Reflect.getOwnMetadataKeys(target);
+ }
+}
diff --git a/src/utils/assignInjectMetadata.ts b/src/repository/assignInjectMetadata.ts
similarity index 82%
rename from src/utils/assignInjectMetadata.ts
rename to src/repository/assignInjectMetadata.ts
index 4a0d6f6..e9c9aac 100644
--- a/src/utils/assignInjectMetadata.ts
+++ b/src/repository/assignInjectMetadata.ts
@@ -1,4 +1,5 @@
-import { InjectTokenDefinition, ParamSource } from "../types";
+import { ParamSource } from "../domain/enums";
+import { InjectionToken, InjectTokenDefinition } from "../domain/types";
/**
* Updates or adds metadata for the injection tokens of a specific function parameter (argument) based on its index and token.
@@ -11,8 +12,7 @@ import { InjectTokenDefinition, ParamSource } from "../types";
export function assignInjectMetadata(
existing: Record,
index: number,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- token?: any
+ token?: InjectionToken
): Record {
const type = ParamSource.INJECT;
diff --git a/src/utils/assignMetadata.ts b/src/repository/assignParamMetadata.ts
similarity index 70%
rename from src/utils/assignMetadata.ts
rename to src/repository/assignParamMetadata.ts
index 3993354..c5d3f3b 100644
--- a/src/utils/assignMetadata.ts
+++ b/src/repository/assignParamMetadata.ts
@@ -1,16 +1,16 @@
-import { ParamDefinition, ParamSource } from "../types";
+import { ParamDefinition } from "../domain/types";
+import { ParamSource } from "../domain/enums";
/**
* Updates parameter metadata with the argument's position (index).
- * Used by an internal factory function to register parameter decorators.
*
* @param existing - The existing parameter metadata.
* @param index - The index of the parameter in the function's argument list.
* @param type - The data source type for the parameter.
- * @param [key] - An optional key to extract a specific value.
+ * @param key - An optional key to extract a specific value.
* @returns The updated parameter metadata.
*/
-export function assignMetadata(
+export function assignParamMetadata(
existing: Record,
index: number,
type: ParamSource,
diff --git a/src/repository/createAppsScriptDecorator.ts b/src/repository/createAppsScriptDecorator.ts
new file mode 100644
index 0000000..1538f3d
--- /dev/null
+++ b/src/repository/createAppsScriptDecorator.ts
@@ -0,0 +1,25 @@
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "../domain/constants";
+import { AppsScriptEventType } from "../domain/enums";
+
+/**
+ * A factory function that creates method decorators for Apps Script events.
+ *
+ * @param {AppsScriptEventType} eventType - The Apps Script event type to be associated with the decorator.
+ * @returns {Function} A function that returns a method decorator.
+ */
+export function createAppsScriptDecorator(eventType: AppsScriptEventType) {
+ return (options: Record = {}): MethodDecorator => {
+ return (
+ _target: object,
+ _key: string | symbol,
+ descriptor: TypedPropertyDescriptor
+ ): TypedPropertyDescriptor => {
+ if (descriptor.value) {
+ Reflect.defineMetadata(APPSSCRIPT_EVENT_METADATA, eventType, descriptor.value);
+ Reflect.defineMetadata(APPSSCRIPT_OPTIONS_METADATA, options, descriptor.value);
+ }
+
+ return descriptor;
+ };
+ };
+}
diff --git a/src/repository/createHttpDecorator.ts b/src/repository/createHttpDecorator.ts
new file mode 100644
index 0000000..8ac1e70
--- /dev/null
+++ b/src/repository/createHttpDecorator.ts
@@ -0,0 +1,25 @@
+import { METHOD_METADATA, PATH_METADATA } from "../domain/constants";
+import { RequestMethod } from "../domain/enums";
+
+/**
+ * A factory function that creates method decorators for HTTP methods.
+ *
+ * @param {RequestMethod} method - The HTTP method to be associated with the decorator.
+ * @returns {Function} A function that returns a method decorator.
+ */
+export function createHttpDecorator(method: RequestMethod) {
+ return (path?: string): MethodDecorator => {
+ return (
+ _target: object,
+ _key: string | symbol,
+ descriptor: TypedPropertyDescriptor
+ ): TypedPropertyDescriptor => {
+ if (descriptor.value) {
+ Reflect.defineMetadata(METHOD_METADATA, method || RequestMethod.GET, descriptor.value);
+ Reflect.defineMetadata(PATH_METADATA, !path ? "/" : path, descriptor.value);
+ }
+
+ return descriptor;
+ };
+ };
+}
diff --git a/src/utils/createMethodDecorator.ts b/src/repository/createMethodDecorator.ts
similarity index 63%
rename from src/utils/createMethodDecorator.ts
rename to src/repository/createMethodDecorator.ts
index 00e9a6f..2687111 100644
--- a/src/utils/createMethodDecorator.ts
+++ b/src/repository/createMethodDecorator.ts
@@ -1,8 +1,5 @@
-import { AppsScriptEventType } from "../types";
-import {
- APPSSCRIPT_EVENT_METADATA,
- APPSSCRIPT_OPTIONS_METADATA
-} from "../config/constants";
+import { AppsScriptEventType } from "../domain/enums";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "../domain/constants";
/**
* Options for Google Apps Script events.
@@ -29,18 +26,15 @@ export function createMethodDecorator(
eventType: AppsScriptEventType,
options?: Omit
): MethodDecorator {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return (target, key, descriptor: TypedPropertyDescriptor) => {
- Reflect.defineMetadata(
- APPSSCRIPT_EVENT_METADATA,
- eventType,
- descriptor.value
- );
- Reflect.defineMetadata(
- APPSSCRIPT_OPTIONS_METADATA,
- options || {},
- descriptor.value
- );
+ return (
+ _target: object,
+ _key: string | symbol,
+ descriptor: TypedPropertyDescriptor
+ ): TypedPropertyDescriptor => {
+ if (descriptor.value) {
+ Reflect.defineMetadata(APPSSCRIPT_EVENT_METADATA, eventType, descriptor.value);
+ Reflect.defineMetadata(APPSSCRIPT_OPTIONS_METADATA, options || {}, descriptor.value);
+ }
return descriptor;
};
diff --git a/src/repository/createParamDecorator.ts b/src/repository/createParamDecorator.ts
new file mode 100644
index 0000000..d4982b3
--- /dev/null
+++ b/src/repository/createParamDecorator.ts
@@ -0,0 +1,31 @@
+import { PARAM_DEFINITIONS_METADATA } from "../domain/constants";
+import { ParamDefinition } from "../domain/types";
+import { ParamSource } from "../domain/enums";
+import { assignParamMetadata } from "../repository";
+
+/**
+ * Creates a parameter decorator with a specified source.
+ *
+ * @param {ParamSource} type - The parameter source type.
+ * @returns {Function} A function that returns a parameter decorator.
+ */
+export function createParamDecorator(type: ParamSource) {
+ return (key?: string): ParameterDecorator => {
+ return (target, propertyKey, parameterIndex) => {
+ const metadataTarget = target;
+
+ const existing: Record =
+ (propertyKey
+ ? Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey)
+ : Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget)) || {};
+
+ const updated = assignParamMetadata(existing, parameterIndex, type, key);
+
+ if (propertyKey) {
+ Reflect.defineMetadata(PARAM_DEFINITIONS_METADATA, updated, metadataTarget, propertyKey);
+ } else {
+ Reflect.defineMetadata(PARAM_DEFINITIONS_METADATA, updated, metadataTarget);
+ }
+ };
+ };
+}
diff --git a/src/repository/getInjectionTokens.ts b/src/repository/getInjectionTokens.ts
new file mode 100644
index 0000000..2821243
--- /dev/null
+++ b/src/repository/getInjectionTokens.ts
@@ -0,0 +1,22 @@
+import { INJECT_TOKENS_METADATA } from "../domain/constants";
+import { InjectTokenDefinition } from "../domain/types";
+
+/**
+ * Retrieves injection tokens associated with a class constructor or a method prototype.
+ *
+ * @param {object} target - The class constructor or the class prototype.
+ * @param {string | symbol} [propertyKey] - The optional property key (method name).
+ * @returns {Record} An object with tokens.
+ */
+export function getInjectionTokens(
+ target: object,
+ propertyKey?: string | symbol
+): Record {
+ const metadataTarget = target;
+
+ if (propertyKey) {
+ return Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget, propertyKey) || {};
+ } else {
+ return Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget) || {};
+ }
+}
diff --git a/src/repository/index.ts b/src/repository/index.ts
new file mode 100644
index 0000000..2fca9d0
--- /dev/null
+++ b/src/repository/index.ts
@@ -0,0 +1,8 @@
+export * from "./MetadataRepository";
+export * from "./assignInjectMetadata";
+export * from "./assignParamMetadata";
+export * from "./createAppsScriptDecorator";
+export * from "./createHttpDecorator";
+export * from "./createMethodDecorator";
+export * from "./createParamDecorator";
+export * from "./getInjectionTokens";
diff --git a/src/service/EventDispatcher.ts b/src/service/EventDispatcher.ts
new file mode 100644
index 0000000..820150d
--- /dev/null
+++ b/src/service/EventDispatcher.ts
@@ -0,0 +1,166 @@
+import {
+ APPSSCRIPT_EVENT_METADATA,
+ APPSSCRIPT_OPTIONS_METADATA,
+ PARAM_DEFINITIONS_METADATA,
+ PARAMTYPES_METADATA
+} from "../domain/constants";
+import { AppsScriptEventType, ParamSource } from "../domain/enums";
+import { InjectTokenDefinition, Newable, ParamDefinition } from "../domain/types";
+import { getInjectionTokens } from "../repository";
+import { Resolver } from "../service";
+
+export class EventDispatcher {
+ constructor(
+ private readonly resolver: Resolver,
+ private readonly controllers: Map
+ ) {}
+
+ public async dispatch(eventType: AppsScriptEventType, event: unknown): Promise {
+ for (const controller of this.controllers.keys()) {
+ const prototype = controller.prototype;
+
+ const propertyNames = Object.getOwnPropertyNames(prototype);
+
+ for (const propertyName of propertyNames) {
+ if (propertyName === "constructor") continue;
+
+ const methodHandler = prototype[propertyName];
+
+ const eventMetadata = Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodHandler);
+
+ const options = Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodHandler);
+
+ if (eventMetadata === eventType && this.checkFilters(eventType, event, options)) {
+ const instance = this.resolver.resolve(controller);
+
+ const args = this.buildMethodParams(instance as object, propertyName, event);
+
+ const handler = (instance as Record)[propertyName] as (
+ ...args: unknown[]
+ ) => unknown;
+ await handler.apply(instance, args);
+ }
+ }
+ }
+ }
+
+ public buildMethodParams(
+ target: object,
+ propertyKey: string | symbol,
+ event: unknown
+ ): unknown[] {
+ const targetPrototype = Object.getPrototypeOf(target);
+
+ const rawMetadata: Record =
+ Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, targetPrototype, propertyKey) || {};
+
+ const rawInjectMetadata: Record = getInjectionTokens(
+ targetPrototype,
+ propertyKey
+ );
+
+ const metadata: (ParamDefinition | InjectTokenDefinition)[] = (
+ Object.values(rawMetadata) as (ParamDefinition | InjectTokenDefinition)[]
+ ).concat(Object.values(rawInjectMetadata) as (ParamDefinition | InjectTokenDefinition)[]);
+
+ metadata.sort((a, b) => a.index - b.index);
+
+ const designParamTypes: Newable[] =
+ Reflect.getMetadata(PARAMTYPES_METADATA, targetPrototype, propertyKey) || [];
+
+ const args: unknown[] = [];
+
+ for (const param of metadata) {
+ switch (param.type) {
+ case ParamSource.EVENT:
+ args[param.index] =
+ param.key && typeof event === "object" && event !== null
+ ? (event as Record)[param.key]
+ : event;
+ break;
+
+ case ParamSource.INJECT:
+ try {
+ const tokenToResolve = "token" in param ? param.token : designParamTypes[param.index];
+
+ if (tokenToResolve) {
+ args[param.index] = this.resolver.resolve(tokenToResolve);
+ } else {
+ args[param.index] = undefined;
+ }
+ } catch {
+ args[param.index] = undefined;
+ }
+ break;
+ }
+ }
+
+ return args;
+ }
+
+ private checkFilters(
+ eventType: AppsScriptEventType,
+ event: unknown,
+ options: Record | undefined
+ ): boolean {
+ if (!options) return true;
+
+ switch (eventType) {
+ case AppsScriptEventType.EDIT:
+ if (options.range) {
+ const editEvent = event as GoogleAppsScript.Events.SheetsOnEdit;
+ const eventRangeA1 =
+ typeof editEvent.range?.getA1Notation === "function"
+ ? editEvent.range.getA1Notation()
+ : null;
+
+ if (!eventRangeA1) {
+ return false;
+ }
+
+ const ranges = Array.isArray(options.range) ? options.range : [options.range];
+
+ // TODO: isRegExp
+ return ranges.some((r: string | RegExp) =>
+ r instanceof RegExp ? r.test(eventRangeA1) : eventRangeA1 === r
+ );
+ }
+ break;
+
+ case AppsScriptEventType.FORM_SUBMIT:
+ if (options.formId) {
+ const submitEvent = event as GoogleAppsScript.Events.FormsOnFormSubmit;
+ const eventFormId = (
+ submitEvent.source as unknown as { getId?: () => string }
+ )?.getId?.();
+
+ if (!eventFormId) {
+ return false;
+ }
+
+ const formIds = Array.isArray(options.formId) ? options.formId : [options.formId];
+
+ return formIds.some((id: string) => eventFormId === id);
+ }
+ break;
+
+ case AppsScriptEventType.CHANGE:
+ if (options.changeType) {
+ const changeEvent = event as GoogleAppsScript.Events.SheetsOnChange;
+ const eventChangeType = changeEvent.changeType;
+
+ if (!eventChangeType) {
+ return false;
+ }
+
+ const changeTypes = Array.isArray(options.changeType)
+ ? options.changeType
+ : [options.changeType];
+
+ return changeTypes.some((type: unknown) => eventChangeType === type);
+ }
+ break;
+ }
+ return true;
+ }
+}
diff --git a/src/service/PathMatcher.ts b/src/service/PathMatcher.ts
new file mode 100644
index 0000000..4cffbec
--- /dev/null
+++ b/src/service/PathMatcher.ts
@@ -0,0 +1,46 @@
+export class PathMatcher {
+ /**
+ * Checks if a given path matches a specified template.
+ *
+ * @param {string} template - The path template (e.g., '/users/{id}').
+ * @param {string} actual - The actual request path (e.g., '/users/123').
+ * @returns {boolean} `true` if the paths match, otherwise `false`.
+ */
+ public match(template: string, actual: string): boolean {
+ const tplParts = template.split("/").filter(Boolean);
+ const actParts = actual.split("/").filter(Boolean);
+
+ if (tplParts.length !== actParts.length) {
+ return false;
+ }
+
+ return tplParts.every((part, i) => {
+ if (part.startsWith("{") && part.endsWith("}")) {
+ return true;
+ }
+ return part === actParts[i];
+ });
+ }
+
+ /**
+ * Extracts path parameters from an actual URL based on a given template.
+ *
+ * @param {string} template - The path template, e.g., "/users/{id}/posts/{postId}".
+ * @param {string} actual - The actual path, e.g., "/users/123/posts/456".
+ * @returns {Record} An object containing the extracted path parameters.
+ */
+ public extractParams(template: string, actual: string): Record {
+ const tplParts = template.split("/").filter(Boolean);
+ const actParts = actual.split("/").filter(Boolean);
+ const params: Record = {};
+
+ tplParts.forEach((part, i) => {
+ if (part.startsWith("{") && part.endsWith("}")) {
+ const paramName = part.slice(1, -1);
+ params[paramName] = actParts[i];
+ }
+ });
+
+ return params;
+ }
+}
diff --git a/src/service/RequestFactory.ts b/src/service/RequestFactory.ts
new file mode 100644
index 0000000..d57a7ac
--- /dev/null
+++ b/src/service/RequestFactory.ts
@@ -0,0 +1,91 @@
+import { isString, normalize } from "apps-script-utils";
+import { HttpHeaders, HttpRequest, ParsedUrl } from "../domain/types";
+import { RequestMethod } from "../domain/enums";
+
+export class RequestFactory {
+ /**
+ * Creates a structured HttpRequest object from a raw Apps Script DoGet or DoPost event.
+ *
+ * @param {RequestMethod} methodRequest - The expected request method.
+ * @param {GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost} event - The raw Apps Script event object.
+ * @returns {HttpRequest} A structured object representing the HTTP request.
+ */
+ public create(
+ methodRequest: RequestMethod,
+ event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost
+ ): HttpRequest {
+ const headers: HttpHeaders =
+ ((input: unknown): HttpHeaders | null => {
+ if (!isString(input)) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(input.trim()) as HttpHeaders;
+ } catch (err: unknown) {
+ console.warn("Failed to parse JSON:", err);
+ }
+
+ return null;
+ })(event?.parameter?.headers) || {};
+
+ const methodParam = event?.parameter?.method?.toLowerCase();
+
+ const method = Object.values(RequestMethod).includes(methodParam as RequestMethod)
+ ? (methodParam as RequestMethod)
+ : methodRequest;
+
+ const rawPathname =
+ event?.pathInfo || event?.parameter?.path || event?.parameter?.pathname || "/";
+
+ const pathname = normalize(rawPathname);
+
+ const search = ((params) => (isString(params) && params.length > 0 ? `?${params}` : undefined))(
+ event?.queryString
+ );
+
+ const url: ParsedUrl = {
+ pathname,
+ path: search ? `${pathname}${search}` : pathname,
+ search,
+ query: event?.parameters ?? {}
+ };
+
+ const rawBody = [
+ RequestMethod.POST,
+ RequestMethod.PUT,
+ RequestMethod.PATCH,
+ RequestMethod.DELETE
+ ].includes(method)
+ ? "postData" in event
+ ? event?.postData?.contents
+ : null
+ : null;
+
+ const body = ((): unknown => {
+ if (!isString(rawBody)) {
+ return rawBody;
+ }
+
+ const contentType =
+ headers["Content-Type"] || ("postData" in event ? event?.postData?.type : undefined) || "";
+
+ if (contentType.includes("application/json")) {
+ try {
+ return JSON.parse(rawBody);
+ } catch (err: unknown) {
+ console.warn("Failed to parse JSON body:", err);
+ }
+ }
+
+ return rawBody;
+ })();
+
+ return {
+ headers,
+ method,
+ url,
+ body
+ };
+ }
+}
diff --git a/src/service/Resolver.ts b/src/service/Resolver.ts
new file mode 100644
index 0000000..602b4ab
--- /dev/null
+++ b/src/service/Resolver.ts
@@ -0,0 +1,93 @@
+import { isFunctionLike } from "apps-script-utils";
+import { InjectionToken, Newable } from "../domain/types";
+import { ParamSource } from "../domain/enums";
+import { PARAMTYPES_METADATA } from "../domain/constants";
+import { getInjectionTokens } from "../repository";
+import { isController, isInjectable } from "../shared/utils";
+
+export class Resolver {
+ constructor(
+ private readonly _controllers: Map,
+ private readonly _providers: Map
+ ) {}
+
+ public resolve(token: InjectionToken): T {
+ if (this._controllers.has(token)) {
+ const instance = this._controllers.get(token);
+
+ if (instance) return instance as T;
+ }
+
+ if (this._providers.has(token)) {
+ const instance = this._providers.get(token);
+
+ if (instance) return instance as T;
+ }
+
+ if (!isFunctionLike(token)) {
+ const tokenName = String(token);
+
+ throw new Error(
+ `[Resolve ERROR]: '${tokenName}' is not registered as a provider or controller.`
+ );
+ }
+
+ const target = token as Newable;
+
+ const designParamTypes: Newable[] = Reflect.getMetadata(PARAMTYPES_METADATA, target) || [];
+
+ const explicitInjectTokens = getInjectionTokens(target);
+
+ const deps = new Array(
+ Math.max(target.length, designParamTypes.length, Object.keys(explicitInjectTokens).length)
+ );
+
+ for (let i = 0; i < deps.length; i++) {
+ const paramKey = `${ParamSource.INJECT}:${i}`;
+
+ const injectDefinition = explicitInjectTokens[paramKey];
+
+ const tokenToResolve = injectDefinition ? injectDefinition.token : designParamTypes[i];
+
+ if (!tokenToResolve) {
+ throw new Error(
+ `[Resolve ERROR]: Dependency at index ${i} of '${target.name}' cannot be resolved (no token).`
+ );
+ }
+
+ if (!this._providers.has(tokenToResolve) && !this._controllers.has(tokenToResolve)) {
+ if (!isFunctionLike(tokenToResolve)) {
+ throw new Error(
+ `[Resolve ERROR]: Invalid injection token at index ${i} of '${target.name}'. Expected a class constructor or a registered token.`
+ );
+ }
+
+ const tokenName =
+ typeof tokenToResolve === "function" ? tokenToResolve.name : String(tokenToResolve);
+
+ throw new Error(
+ `[Resolve ERROR]: '${tokenName}' is not registered as a provider or controller.`
+ );
+ }
+
+ deps[i] = isFunctionLike(tokenToResolve)
+ ? this.resolve(tokenToResolve as Newable)
+ : this._providers.get(tokenToResolve);
+ }
+
+ const TargetClass = target as unknown as new (...args: unknown[]) => T;
+ const instance = new TargetClass(...deps);
+
+ if (isController(target)) {
+ this._controllers.set(target, instance);
+ } else if (isInjectable(target)) {
+ this._providers.set(target, instance);
+ } else {
+ console.warn(
+ `[Resolve ERROR]: '${target.name}' is not registered as a provider or controller.`
+ );
+ }
+
+ return instance;
+ }
+}
diff --git a/src/service/ResponseBuilder.ts b/src/service/ResponseBuilder.ts
new file mode 100644
index 0000000..3a97ad1
--- /dev/null
+++ b/src/service/ResponseBuilder.ts
@@ -0,0 +1,82 @@
+import { HeaderAcceptMimeType, HttpStatus, RequestMethod } from "../domain/enums";
+import { HttpHeaders, HttpRequest, HttpResponse } from "../domain/types";
+
+export class ResponseBuilder {
+ /**
+ * Creates a structured HttpResponse object.
+ *
+ * @param {HttpRequest} request - The original request object.
+ * @param {HttpStatus} [status] - The desired HTTP status code.
+ * @param {HttpHeaders} [headers] - Optional custom headers.
+ * @param {unknown} [data] - The data to be sent in the response body.
+ * @returns {HttpResponse} A structured object representing the HTTP response.
+ */
+ public create(
+ request: HttpRequest,
+ status: HttpStatus | undefined,
+ headers: HttpHeaders | undefined = {},
+ data: unknown = null
+ ): HttpResponse {
+ const resolvedStatus =
+ status ??
+ ([RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS].includes(request.method)
+ ? HttpStatus.OK
+ : HttpStatus.CREATED);
+
+ const statusText = ((): string => {
+ const entry = Object.entries(HttpStatus).find(([, value]) => value === resolvedStatus);
+
+ return entry ? entry[0] : "UNKNOWN_STATUS";
+ })();
+
+ const ok = resolvedStatus >= 200 && resolvedStatus < 300;
+
+ return {
+ headers,
+ ok,
+ status: resolvedStatus,
+ statusText,
+ body: ok ? data : { error: data }
+ };
+ }
+
+ /**
+ * Wraps a HttpResponse object into a format suitable for return from Apps Script entry points.
+ *
+ * @param {HttpRequest} request - The structured request object.
+ * @param {HttpResponse} response - The structured response object to be wrapped.
+ * @returns {string | GoogleAppsScript.Content.TextOutput | GoogleAppsScript.HTML.HtmlOutput} A value that Apps Script can return directly to the client.
+ */
+ public wrap(
+ request: HttpRequest,
+ response: HttpResponse
+ ): string | GoogleAppsScript.Content.TextOutput | GoogleAppsScript.HTML.HtmlOutput {
+ const mimeType = (request.headers?.Accept as HeaderAcceptMimeType) || HeaderAcceptMimeType.HTML;
+
+ response.headers["Content-Type"] = mimeType;
+
+ const isApi = request.url.pathname?.startsWith("/api/") || false;
+
+ const result = JSON.stringify(isApi ? response : response.body);
+
+ switch (mimeType) {
+ case HeaderAcceptMimeType.GOOGLE_JSON:
+ return result;
+
+ case HeaderAcceptMimeType.GOOGLE_TEXT:
+ return result;
+
+ case HeaderAcceptMimeType.JSON:
+ return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
+
+ case HeaderAcceptMimeType.TEXT:
+ return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.TEXT);
+
+ case HeaderAcceptMimeType.HTML:
+ return HtmlService.createHtmlOutput(result);
+
+ default:
+ return HtmlService.createHtmlOutput(result);
+ }
+ }
+}
diff --git a/src/service/Router.ts b/src/service/Router.ts
new file mode 100644
index 0000000..f490eb5
--- /dev/null
+++ b/src/service/Router.ts
@@ -0,0 +1,178 @@
+import { isObject } from "apps-script-utils";
+import {
+ HttpHeaders,
+ HttpRequest,
+ HttpResponse,
+ InjectTokenDefinition,
+ Newable,
+ ParamDefinition,
+ RouteMetadata
+} from "../domain/types";
+import { PARAM_DEFINITIONS_METADATA, PARAMTYPES_METADATA } from "../domain/constants";
+import { ParamSource } from "../domain/enums";
+import { RouteExecutionContext } from "../domain/entities";
+import { getInjectionTokens } from "../repository";
+import { PathMatcher, Resolver } from "../service";
+
+export class Router {
+ private readonly pathMatcher = new PathMatcher();
+
+ constructor(
+ private readonly _resolver: Resolver,
+ private readonly _routes: RouteMetadata[]
+ ) {}
+
+ public async handle(
+ request: HttpRequest,
+ event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost,
+ responseBuilder: (
+ request: HttpRequest,
+ status?: number,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ ): Promise {
+ const route = this._routes.find(
+ (r) => r.method === request.method && this.pathMatcher.match(r.path, request.url.pathname)
+ );
+
+ if (!route) {
+ return responseBuilder(
+ request,
+ 404,
+ {},
+ { message: `Cannot ${request.method} ${request.url.pathname}` }
+ );
+ }
+
+ const controllerInstance = this._resolver.resolve(route.controller);
+
+ const params = this.pathMatcher.extractParams(route.path, request.url.pathname);
+
+ const ctx: RouteExecutionContext = {
+ event,
+ params,
+ query: request.url.query,
+ request,
+ headers: request.headers,
+ body: request.body,
+ response: responseBuilder(request, undefined, {}, null)
+ };
+
+ const args = this.buildMethodParams(controllerInstance as object, route.handler, ctx);
+
+ try {
+ const handler = (controllerInstance as Record)[route.handler] as (
+ ...args: unknown[]
+ ) => unknown;
+ const result = await handler.apply(controllerInstance, args);
+
+ if (isObject(result) && "status" in result && "body" in result) {
+ return result as HttpResponse;
+ }
+
+ return responseBuilder(request, ctx.response?.status, ctx.response?.headers, result);
+ } catch (err: unknown) {
+ const status = (err as { status?: number })?.status || 500;
+ const message = (err as { message?: string })?.message || String(err);
+ return responseBuilder(request, status, {}, message);
+ }
+ }
+
+ private buildMethodParams(
+ target: object,
+ propertyKey: string | symbol,
+ ctx: RouteExecutionContext
+ ): unknown[] {
+ const targetPrototype = Object.getPrototypeOf(target);
+
+ const rawMetadata: Record =
+ (Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, targetPrototype, propertyKey) as Record<
+ string,
+ ParamDefinition
+ >) || {};
+
+ const rawInjectMetadata: Record = getInjectionTokens(
+ targetPrototype,
+ propertyKey
+ );
+
+ const metadata: (ParamDefinition | InjectTokenDefinition)[] = (
+ Object.values(rawMetadata) as (ParamDefinition | InjectTokenDefinition)[]
+ ).concat(Object.values(rawInjectMetadata) as (ParamDefinition | InjectTokenDefinition)[]);
+
+ metadata.sort((a, b) => a.index - b.index);
+
+ const designParamTypes: Newable[] =
+ (Reflect.getMetadata(PARAMTYPES_METADATA, targetPrototype, propertyKey) as Newable[]) || [];
+
+ const args: unknown[] = [];
+
+ for (const param of metadata) {
+ switch (param.type) {
+ case ParamSource.PARAM:
+ args[param.index] = param.key ? (ctx.params ?? {})[param.key] : ctx.params;
+ break;
+
+ case ParamSource.QUERY:
+ args[param.index] = param.key ? (ctx.query ?? {})[param.key] : ctx.query;
+ break;
+
+ case ParamSource.BODY:
+ args[param.index] =
+ param.key && ctx.body && isObject(ctx.body)
+ ? (ctx.body as unknown as Record)[param.key]
+ : ctx.body;
+ break;
+
+ case ParamSource.EVENT:
+ args[param.index] =
+ param.key && isObject(ctx.event)
+ ? (ctx.event as unknown as Record)[param.key]
+ : ctx.event;
+ break;
+
+ case ParamSource.REQUEST:
+ args[param.index] =
+ param.key && isObject(ctx.request)
+ ? (ctx.request as unknown as Record)[param.key]
+ : ctx.request;
+ break;
+
+ case ParamSource.HEADERS:
+ if (param.key && ctx.headers) {
+ const headerKey = Object.keys(ctx.headers).find(
+ (k) => k.toLowerCase() === param.key!.toLowerCase()
+ );
+ args[param.index] = headerKey ? ctx.headers[headerKey] : undefined;
+ } else {
+ args[param.index] = ctx.headers;
+ }
+ break;
+
+ case ParamSource.RESPONSE:
+ args[param.index] =
+ param.key && isObject(ctx.response)
+ ? (ctx.response as unknown as Record)[param.key]
+ : ctx.response;
+ break;
+
+ case ParamSource.INJECT:
+ try {
+ const tokenToResolve = "token" in param ? param.token : designParamTypes[param.index];
+
+ if (tokenToResolve) {
+ args[param.index] = this._resolver.resolve(tokenToResolve);
+ } else {
+ args[param.index] = undefined;
+ }
+ } catch {
+ args[param.index] = undefined;
+ }
+ break;
+ }
+ }
+
+ return args;
+ }
+}
diff --git a/src/utils/RouterExplorer.ts b/src/service/RouterExplorer.ts
similarity index 70%
rename from src/utils/RouterExplorer.ts
rename to src/service/RouterExplorer.ts
index 3ca3beb..dbb46b9 100644
--- a/src/utils/RouterExplorer.ts
+++ b/src/service/RouterExplorer.ts
@@ -4,11 +4,11 @@ import {
CONTROLLER_TYPE_METADATA,
METHOD_METADATA,
PATH_METADATA
-} from "../config/constants";
-import { Newable, RouteMetadata } from "../types";
+} from "../domain/constants";
+import { Newable, RouteMetadata } from "../domain/types";
export class RouterExplorer {
- static explore(controllers: Map): RouteMetadata[] {
+ public explore(controllers: Map): RouteMetadata[] {
const routes: RouteMetadata[] = [];
for (const controller of controllers.keys()) {
@@ -17,25 +17,28 @@ export class RouterExplorer {
const isHttpController = controllerType === "http";
- if (!isHttpController) continue;
+ if (!isHttpController) {
+ continue;
+ }
- const controllerOptions =
- Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, controller) || {};
+ const controllerOptions = Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, controller) || {};
const basePath = controllerOptions.basePath || "/";
const prototype = controller.prototype;
+
const propertyNames = Object.getOwnPropertyNames(prototype);
for (const propertyName of propertyNames) {
- if (propertyName === "constructor") continue;
+ if (propertyName === "constructor") {
+ continue;
+ }
const methodHandler = prototype[propertyName];
+
const routePath = Reflect.getMetadata(PATH_METADATA, methodHandler);
- const requestMethod = Reflect.getMetadata(
- METHOD_METADATA,
- methodHandler
- );
+
+ const requestMethod = Reflect.getMetadata(METHOD_METADATA, methodHandler);
if (routePath && requestMethod) {
routes.push({
diff --git a/src/service/index.ts b/src/service/index.ts
new file mode 100644
index 0000000..8c58542
--- /dev/null
+++ b/src/service/index.ts
@@ -0,0 +1,7 @@
+export * from "./EventDispatcher";
+export * from "./PathMatcher";
+export * from "./RequestFactory";
+export * from "./Resolver";
+export * from "./ResponseBuilder";
+export * from "./Router";
+export * from "./RouterExplorer";
diff --git a/src/utils/createRequest.ts b/src/shared/utils/createRequest.ts
similarity index 76%
rename from src/utils/createRequest.ts
rename to src/shared/utils/createRequest.ts
index cac5329..ddcb88b 100644
--- a/src/utils/createRequest.ts
+++ b/src/shared/utils/createRequest.ts
@@ -1,5 +1,6 @@
import { isString, normalize } from "apps-script-utils";
-import { HttpHeaders, HttpRequest, ParsedUrl, RequestMethod } from "../types";
+import { HttpHeaders, HttpRequest, ParsedUrl } from "../../domain/types";
+import { RequestMethod } from "../../domain/enums";
/**
* Creates a structured {@link HttpRequest} object from a raw Apps Script `DoGet` or `DoPost` event.
@@ -14,14 +15,13 @@ export function createRequest(
event: GoogleAppsScript.Events.DoGet | GoogleAppsScript.Events.DoPost
): HttpRequest {
const headers: HttpHeaders =
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ((input: unknown): any => {
+ ((input: unknown): HttpHeaders | null => {
if (!isString(input)) {
return null;
}
try {
- return JSON.parse(input.trim());
+ return JSON.parse(input.trim()) as HttpHeaders;
} catch (err: unknown) {
console.warn("Failed to parse JSON:", err);
}
@@ -30,21 +30,14 @@ export function createRequest(
})(event?.parameter?.headers) || {};
const methodParam = event?.parameter?.method?.toLowerCase();
- const method = Object.values(RequestMethod).includes(
- methodParam as RequestMethod
- )
+ const method = Object.values(RequestMethod).includes(methodParam as RequestMethod)
? (methodParam as RequestMethod)
: methodRequest;
- const rawPathname =
- event?.pathInfo ||
- event?.parameter?.path ||
- event?.parameter?.pathname ||
- "";
+ const rawPathname = event?.pathInfo || event?.parameter?.path || event?.parameter?.pathname || "";
const pathname = normalize(rawPathname);
- const search = (params =>
- isString(params) && params.length > 0 ? `?${params}` : undefined)(
+ const search = ((params) => (isString(params) && params.length > 0 ? `?${params}` : undefined))(
event?.queryString
);
diff --git a/src/utils/createResponse.ts b/src/shared/utils/createResponse.ts
similarity index 84%
rename from src/utils/createResponse.ts
rename to src/shared/utils/createResponse.ts
index ae5015a..a3d4fcb 100644
--- a/src/utils/createResponse.ts
+++ b/src/shared/utils/createResponse.ts
@@ -1,10 +1,5 @@
-import {
- HttpHeaders,
- HttpRequest,
- HttpResponse,
- HttpStatus,
- RequestMethod
-} from "../types";
+import { HttpHeaders, HttpRequest, HttpResponse } from "../../domain/types";
+import { HttpStatus, RequestMethod } from "../../domain/enums";
/**
* Creates a structured {@link HttpResponse} object based on the incoming request, a desired HTTP status, headers, and response data.
@@ -27,16 +22,12 @@ export function createResponse(
): HttpResponse {
const resolvedStatus =
status ??
- ([RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS].includes(
- request.method
- )
+ ([RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS].includes(request.method)
? HttpStatus.OK
: HttpStatus.CREATED);
const statusText = ((): string => {
- const entry = Object.entries(HttpStatus).find(
- ([, value]) => value === resolvedStatus
- );
+ const entry = Object.entries(HttpStatus).find(([, value]) => value === resolvedStatus);
return entry ? entry[0] : "UNKNOWN_STATUS";
})();
diff --git a/src/utils/extractPathParams.ts b/src/shared/utils/extractPathParams.ts
similarity index 88%
rename from src/utils/extractPathParams.ts
rename to src/shared/utils/extractPathParams.ts
index d96d179..3568f8d 100644
--- a/src/utils/extractPathParams.ts
+++ b/src/shared/utils/extractPathParams.ts
@@ -6,10 +6,7 @@
* @returns An object containing the extracted path parameters.
* For example, for the above inputs, it might return `{ id: "123", postId: "456" }`.
*/
-export function extractPathParams(
- template: string,
- actual: string
-): Record {
+export function extractPathParams(template: string, actual: string): Record {
const tplParts = template.split("/").filter(Boolean);
const actParts = actual.split("/").filter(Boolean);
const params: Record = {};
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
new file mode 100644
index 0000000..32f897f
--- /dev/null
+++ b/src/shared/utils/index.ts
@@ -0,0 +1,7 @@
+export * from "./createRequest";
+export * from "./createResponse";
+export * from "./extractPathParams";
+export * from "./isController";
+export * from "./isInjectable";
+export * from "./pathMatch";
+export * from "./wrapResponse";
diff --git a/src/utils/isController.ts b/src/shared/utils/isController.ts
similarity index 52%
rename from src/utils/isController.ts
rename to src/shared/utils/isController.ts
index 6352652..fb58f55 100644
--- a/src/utils/isController.ts
+++ b/src/shared/utils/isController.ts
@@ -1,5 +1,5 @@
-import { CONTROLLER_WATERMARK } from "../config/constants";
-import { Newable } from "../types";
+import { CONTROLLER_WATERMARK } from "../../domain/constants";
+import { Newable } from "../../domain/types";
export function isController(value: Newable): boolean {
return !!Reflect.getMetadata(CONTROLLER_WATERMARK, value);
diff --git a/src/utils/isInjectable.ts b/src/shared/utils/isInjectable.ts
similarity index 52%
rename from src/utils/isInjectable.ts
rename to src/shared/utils/isInjectable.ts
index 269bc57..c80830a 100644
--- a/src/utils/isInjectable.ts
+++ b/src/shared/utils/isInjectable.ts
@@ -1,5 +1,5 @@
-import { INJECTABLE_WATERMARK } from "../config/constants";
-import { Newable } from "../types";
+import { INJECTABLE_WATERMARK } from "../../domain/constants";
+import { Newable } from "../../domain/types";
export function isInjectable(value: Newable): boolean {
return !!Reflect.hasMetadata(INJECTABLE_WATERMARK, value);
diff --git a/src/utils/pathMatch.ts b/src/shared/utils/pathMatch.ts
similarity index 100%
rename from src/utils/pathMatch.ts
rename to src/shared/utils/pathMatch.ts
diff --git a/src/utils/wrapResponse.ts b/src/shared/utils/wrapResponse.ts
similarity index 77%
rename from src/utils/wrapResponse.ts
rename to src/shared/utils/wrapResponse.ts
index aa39969..43ef5a4 100644
--- a/src/utils/wrapResponse.ts
+++ b/src/shared/utils/wrapResponse.ts
@@ -1,4 +1,5 @@
-import { HeaderAcceptMimeType, HttpRequest, HttpResponse } from "../types";
+import { HeaderAcceptMimeType } from "../../domain/enums";
+import { HttpRequest, HttpResponse } from "../../domain/types";
/**
* Wraps a {@link HttpResponse} object into a format suitable for return from Apps Script entry point functions (e.g., `doGet`, `doPost`).
@@ -12,13 +13,8 @@ import { HeaderAcceptMimeType, HttpRequest, HttpResponse } from "../types";
export function wrapResponse(
request: HttpRequest,
response: HttpResponse
-):
- | string
- | GoogleAppsScript.Content.TextOutput
- | GoogleAppsScript.HTML.HtmlOutput {
- const mimeType =
- (request.headers?.Accept as HeaderAcceptMimeType) ||
- HeaderAcceptMimeType.HTML;
+): string | GoogleAppsScript.Content.TextOutput | GoogleAppsScript.HTML.HtmlOutput {
+ const mimeType = (request.headers?.Accept as HeaderAcceptMimeType) || HeaderAcceptMimeType.HTML;
response.headers["Content-Type"] = mimeType;
@@ -33,14 +29,10 @@ export function wrapResponse(
return result;
case HeaderAcceptMimeType.JSON:
- return ContentService.createTextOutput(result).setMimeType(
- ContentService.MimeType.JSON
- );
+ return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
case HeaderAcceptMimeType.TEXT:
- return ContentService.createTextOutput(result).setMimeType(
- ContentService.MimeType.TEXT
- );
+ return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.TEXT);
case HeaderAcceptMimeType.HTML:
return HtmlService.createHtmlOutput(result);
diff --git a/src/types/AppsScriptEventType.ts b/src/types/AppsScriptEventType.ts
deleted file mode 100644
index 522dc58..0000000
--- a/src/types/AppsScriptEventType.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export enum AppsScriptEventType {
- INSTALL = "INSTALL",
- OPEN = "OPEN",
- EDIT = "EDIT",
- CHANGE = "CHANGE",
- SELECTION_CHANGE = "SELECTION_CHANGE",
- FORM_SUBMIT = "FORM_SUBMIT"
-}
diff --git a/src/types/HeaderAcceptMimeType.ts b/src/types/HeaderAcceptMimeType.ts
deleted file mode 100644
index 7c155a4..0000000
--- a/src/types/HeaderAcceptMimeType.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export enum HeaderAcceptMimeType {
- GOOGLE_TEXT = "google/plain",
- GOOGLE_JSON = "google/json",
- HTML = "text/html",
- TEXT = "text/plain",
- JSON = "application/json"
-}
diff --git a/src/types/HttpException.ts b/src/types/HttpException.ts
deleted file mode 100644
index bdc148c..0000000
--- a/src/types/HttpException.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface HttpException {
- cause?: unknown;
- description?: string;
-}
diff --git a/src/types/InjectTokenDefinition.ts b/src/types/InjectTokenDefinition.ts
deleted file mode 100644
index 5f8627c..0000000
--- a/src/types/InjectTokenDefinition.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ParamSource } from "./ParamSource";
-
-/**
- *
- */
-export interface InjectTokenDefinition {
- type: ParamSource.INJECT;
- index: number;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- token: any;
-}
diff --git a/src/types/Newable.ts b/src/types/Newable.ts
deleted file mode 100644
index cd82a6b..0000000
--- a/src/types/Newable.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type Newable = new (...args: any[]) => T;
diff --git a/src/types/ParamDefinition.ts b/src/types/ParamDefinition.ts
deleted file mode 100644
index 82235c3..0000000
--- a/src/types/ParamDefinition.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { ParamSource } from "./ParamSource";
-
-export interface ParamDefinition {
- type: ParamSource;
- index: number;
- key: string | null | undefined;
-}
diff --git a/src/types/ParsedUrl.ts b/src/types/ParsedUrl.ts
deleted file mode 100644
index 9ad46a6..0000000
--- a/src/types/ParsedUrl.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ParsedUrlQuery } from "./ParsedUrlQuery";
-
-export interface ParsedUrl {
- path?: string | undefined;
- pathname?: string | undefined;
- search?: string | undefined;
- query?: string | undefined | ParsedUrlQuery;
-}
diff --git a/src/types/Provider.ts b/src/types/Provider.ts
deleted file mode 100644
index 2a147e8..0000000
--- a/src/types/Provider.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Newable } from "./Newable";
-
-export type Provider = Newable;
diff --git a/src/types/google-apps-script.d.ts b/src/types/google-apps-script.d.ts
deleted file mode 100644
index 4bca2a4..0000000
--- a/src/types/google-apps-script.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-declare global {
- namespace GoogleAppsScript {
- namespace Events {
- interface SheetsOnSelectionChange extends AppsScriptEvent {
- source: GoogleAppsScript.Spreadsheet.Spreadsheet;
- range: GoogleAppsScript.Spreadsheet.Range;
- }
- }
- }
-}
-
-export {};
diff --git a/src/types/index.ts b/src/types/index.ts
deleted file mode 100644
index 6552043..0000000
--- a/src/types/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export { type AppConfig } from "./AppConfig";
-export { AppsScriptEventType } from "./AppsScriptEventType";
-export { type Newable } from "./Newable";
-export { type ErrorResponse } from "./ErrorResponse";
-export { HeaderAcceptMimeType } from "./HeaderAcceptMimeType";
-export { type HttpException } from "./HttpException";
-export { type HttpHeaders } from "./HttpHeaders";
-export { type HttpRequest } from "./HttpRequest";
-export { type HttpResponse } from "./HttpResponse";
-export { HttpStatus } from "./HttpStatus";
-export { type InjectTokenDefinition } from "./InjectTokenDefinition";
-export { type ParamDefinition } from "./ParamDefinition";
-export { ParamSource } from "./ParamSource";
-export { type ParsedUrl } from "./ParsedUrl";
-export { type ParsedUrlQuery } from "./ParsedUrlQuery";
-export { type Provider } from "./Provider";
-export { type RouteMetadata } from "./RouteMetadata";
-export { RequestMethod } from "./RequestMethod";
diff --git a/src/utils/buildMethodParams.ts b/src/utils/buildMethodParams.ts
deleted file mode 100644
index 8b295af..0000000
--- a/src/utils/buildMethodParams.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { isObject } from "apps-script-utils";
-import {
- PARAM_DEFINITIONS_METADATA,
- PARAMTYPES_METADATA
-} from "../config/constants";
-import {
- HttpHeaders,
- HttpRequest,
- HttpResponse,
- InjectTokenDefinition,
- Newable,
- ParamDefinition,
- ParamSource
-} from "../types";
-import { getInjectionTokens, resolve } from "../utils";
-
-/**
- * Creates an array of arguments for invoking a controller method, based on parameter metadata and the provided context.
- *
- * This method inspects the metadata associated with the target method's parameters (e.g., `@Param`, `@Query`, `@Body`, `@Inject`) to populate the arguments array from the `ctx` (context) object.
- *
- * @param target - The controller instance (the target object).
- * @param propertyKey - The name of the method to be invoked.
- * @param ctx - The context object containing event data.
- * @param ctx.event - The raw Apps Script event object.
- * @param [ctx.params] - An object containing extracted path parameters (e.g., from a URL like `/users/{id}`).
- * @param [ctx.query] - An object containing query parameters (e.g., from a URL like `?name=value`).
- * @param [ctx.body] - The raw request body content, typically a JSON string for POST requests.
- * @param [ctx.request] - A structured request object derived from the Apps Script event.
- * @param [ctx.headers] - An object containing request headers.
- * @param [ctx.response] - A structured response object.
- * @param controllers
- * @param providers
- * @returns An array of arguments, ready to be passed into the controller method.
- */
-export function buildMethodParams(
- target: object,
- propertyKey: string | symbol,
- ctx: {
- event:
- | GoogleAppsScript.Events.DoGet
- | GoogleAppsScript.Events.DoPost
- | GoogleAppsScript.Events.AddonOnInstall
- | GoogleAppsScript.Events.DocsOnOpen
- | GoogleAppsScript.Events.SlidesOnOpen
- | GoogleAppsScript.Events.SheetsOnOpen
- | GoogleAppsScript.Events.FormsOnOpen
- | GoogleAppsScript.Events.SheetsOnEdit
- | GoogleAppsScript.Events.SheetsOnChange
- | GoogleAppsScript.Events.SheetsOnSelectionChange
- | GoogleAppsScript.Events.FormsOnFormSubmit;
- params?: Record;
- query?: Record;
- request?: HttpRequest;
- headers?: HttpHeaders;
- body?: string | object | null | undefined;
- response?: HttpResponse;
- },
- controllers: Map,
- providers: Map
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
-): any[] {
- const targetPrototype = Object.getPrototypeOf(target);
-
- const rawMetadata: Record =
- Reflect.getMetadata(
- PARAM_DEFINITIONS_METADATA,
- targetPrototype,
- propertyKey
- ) || {};
-
- const rawInjectMetadata: Record =
- getInjectionTokens(targetPrototype, propertyKey);
-
- const metadata: (ParamDefinition | InjectTokenDefinition)[] = (
- Object.values(rawMetadata) as (ParamDefinition | InjectTokenDefinition)[]
- ).concat(
- Object.values(rawInjectMetadata) as (
- | ParamDefinition
- | InjectTokenDefinition
- )[]
- );
-
- metadata.sort((a, b) => a.index - b.index);
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const designParamTypes: Newable[] =
- Reflect.getMetadata(PARAMTYPES_METADATA, targetPrototype, propertyKey) ||
- [];
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const args: any[] = [];
-
- for (const param of metadata) {
- switch (param.type) {
- case ParamSource.PARAM:
- args[param.index] = param.key
- ? (ctx.params ?? {})[param.key]
- : ctx.params;
- break;
-
- case ParamSource.QUERY:
- args[param.index] = param.key
- ? (ctx.query ?? {})[param.key]
- : ctx.query;
- break;
-
- case ParamSource.BODY:
- args[param.index] =
- param.key && ctx.body && isObject(ctx.body)
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (ctx.body as Record)[param.key]
- : ctx.body;
- break;
-
- case ParamSource.EVENT:
- args[param.index] = param.key
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (ctx.event as any)[param.key]
- : ctx.event;
- break;
-
- case ParamSource.REQUEST:
- args[param.index] = param.key
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (ctx.request as any)?.[param.key]
- : ctx.request;
- break;
-
- case ParamSource.HEADERS:
- if (param.key && ctx.headers) {
- const headerKey = Object.keys(ctx.headers).find(
- k => k.toLowerCase() === param.key!.toLowerCase()
- );
- args[param.index] = headerKey ? ctx.headers[headerKey] : undefined;
- } else {
- args[param.index] = ctx.headers;
- }
- break;
-
- case ParamSource.RESPONSE:
- args[param.index] = param.key
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (ctx.response as any)?.[param.key]
- : ctx.response;
- break;
-
- case ParamSource.INJECT:
- try {
- const tokenToResolve =
- "token" in param ? param.token : designParamTypes[param.index];
-
- if (tokenToResolve) {
- args[param.index] = resolve(controllers, providers, tokenToResolve);
- } else {
- args[param.index] = undefined;
- }
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- } catch (err: unknown) {
- args[param.index] = undefined;
- }
- break;
- }
- }
-
- return args;
-}
diff --git a/src/utils/checkEventFilters.ts b/src/utils/checkEventFilters.ts
deleted file mode 100644
index 3457a5a..0000000
--- a/src/utils/checkEventFilters.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { AppsScriptEventType } from "../types";
-
-/**
- * Checks if an Apps Script event matches specific filters defined in method options.
- * This allows event handlers to be triggered conditionally based on properties of the event, such as the edited range in a sheet, the ID of a submitted form, or the type of change.
- *
- * @param eventType - The type of Apps Script event (e.g., EDIT, FORM_SUBMIT).
- * @param event - The raw Apps Script event object. Its type varies based on `eventType`.
- * @param methodOptions - An object containing filtering options for the event.
- * @returns `true` if the event matches the specified filters or if no filters are defined, otherwise `false`.
- */
-export function checkEventFilters(
- eventType: AppsScriptEventType,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- event: any,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- methodOptions: Record | undefined
-): boolean {
- if (!methodOptions) {
- return true;
- }
-
- switch (eventType) {
- case AppsScriptEventType.EDIT:
- if (methodOptions.range) {
- const eventRangeA1 = (
- event as GoogleAppsScript.Events.SheetsOnEdit
- ).range?.getA1Notation();
-
- if (!eventRangeA1) {
- return false;
- }
-
- const ranges = Array.isArray(methodOptions.range)
- ? methodOptions.range
- : [methodOptions.range];
-
- return ranges.some((r: string | RegExp) => {
- if (r instanceof RegExp) {
- return r.test(eventRangeA1);
- }
-
- return eventRangeA1 === r;
- });
- }
- break;
-
- case AppsScriptEventType.FORM_SUBMIT:
- if (methodOptions.formId) {
- const eventFormId = (
- event as GoogleAppsScript.Events.FormsOnFormSubmit
- ).source?.getId?.();
-
- if (!eventFormId) {
- return false;
- }
-
- const formIds = Array.isArray(methodOptions.formId)
- ? methodOptions.formId
- : [methodOptions.formId];
-
- return formIds.some((id: string) => eventFormId === id);
- }
- break;
-
- case AppsScriptEventType.CHANGE:
- if (methodOptions.changeType) {
- const eventChangeType = (
- event as GoogleAppsScript.Events.SheetsOnChange
- ).changeType;
-
- if (!eventChangeType) {
- return false;
- }
-
- const changeTypes = Array.isArray(methodOptions.changeType)
- ? methodOptions.changeType
- : [methodOptions.changeType];
-
- return changeTypes.some(type => eventChangeType === type);
- }
- break;
- }
-
- return true;
-}
diff --git a/src/utils/createHttpDecorator.ts b/src/utils/createHttpDecorator.ts
deleted file mode 100644
index f9825f1..0000000
--- a/src/utils/createHttpDecorator.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { RequestMethod } from "../types";
-import { METHOD_METADATA, PATH_METADATA } from "../config/constants";
-
-/**
- * A factory function that creates method decorators for HTTP methods.
- * It is not intended for direct use.
- *
- * @param method - The HTTP method to be associated with the decorator.
- * @returns A function that returns a method decorator.
- * @environment `Google Apps Script`
- */
-export function createHttpDecorator(method: RequestMethod) {
- return (path?: string): MethodDecorator => {
- return (
- target: object,
- key: string | symbol,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- descriptor: TypedPropertyDescriptor
- ) => {
- Reflect.defineMetadata(
- METHOD_METADATA,
- method || RequestMethod.GET,
- descriptor.value
- );
-
- Reflect.defineMetadata(
- PATH_METADATA,
- !path ? "/" : path,
- descriptor.value
- );
-
- return descriptor;
- };
- };
-}
diff --git a/src/utils/createParamDecorator.ts b/src/utils/createParamDecorator.ts
deleted file mode 100644
index e3b87f8..0000000
--- a/src/utils/createParamDecorator.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ParamDefinition, ParamSource } from "../types";
-import { PARAM_DEFINITIONS_METADATA } from "../config/constants";
-import { assignMetadata } from "../utils";
-
-/**
- * Creates a parameter decorator with a specified source.
- */
-export function createParamDecorator(type: ParamSource) {
- return (key?: string): ParameterDecorator => {
- return (target, propertyKey, parameterIndex) => {
- const metadataTarget = propertyKey ? target : target.constructor;
-
- const existing: Record =
- (propertyKey
- ? Reflect.getMetadata(
- PARAM_DEFINITIONS_METADATA,
- metadataTarget,
- propertyKey
- )
- : Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget)) ||
- {};
-
- const updated = assignMetadata(existing, parameterIndex, type, key);
-
- if (propertyKey) {
- Reflect.defineMetadata(
- PARAM_DEFINITIONS_METADATA,
- updated,
- metadataTarget,
- propertyKey
- );
- } else {
- Reflect.defineMetadata(
- PARAM_DEFINITIONS_METADATA,
- updated,
- metadataTarget
- );
- }
- };
- };
-}
diff --git a/src/utils/getInjectionTokens.ts b/src/utils/getInjectionTokens.ts
deleted file mode 100644
index 1063fc4..0000000
--- a/src/utils/getInjectionTokens.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { InjectTokenDefinition } from "../types";
-import { INJECT_TOKENS_METADATA } from "../config/constants";
-
-/**
- * Retrieves injection tokens associated with a class constructor or a method prototype.
- *
- * @param target - The class constructor (for constructor parameters) or the class prototype (for method parameters).
- * @param [propertyKey] - The optional property key (method name) if tokens are being injected into method parameters.
- * @returns An object with tokens, where the key is a string "${type}:${index}".
- * @environment `Google Apps Script`
- */
-export function getInjectionTokens(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- target: any,
- propertyKey?: string | symbol
-): Record {
- const metadataTarget =
- typeof target === "function" ? target : target.constructor;
-
- if (propertyKey) {
- return (
- Reflect.getMetadata(
- INJECT_TOKENS_METADATA,
- metadataTarget,
- propertyKey
- ) || {}
- );
- } else {
- return Reflect.getMetadata(INJECT_TOKENS_METADATA, metadataTarget) || {};
- }
-}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 398d265..b812aca 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,24 +1,23 @@
-export { buildMethodParams } from "./buildMethodParams";
-export { checkEventFilters } from "./checkEventFilters";
-export { createRequest } from "./createRequest";
-export { createResponse } from "./createResponse";
-export { extractPathParams } from "./extractPathParams";
-export { getInjectionTokens } from "./getInjectionTokens";
-export { isController } from "./isController";
-export { isInjectable } from "./isInjectable";
-export { pathMatch } from "./pathMatch";
-export { resolve } from "./resolve";
-export { RouterExplorer } from "./RouterExplorer";
-export { wrapResponse } from "./wrapResponse";
+import { InjectionToken, Newable } from "../domain/types";
+import { EventDispatcher, Resolver } from "../service";
-// decorators/class
+export function resolve(
+ controllers: Map,
+ providers: Map,
+ token: InjectionToken
+): T {
+ const resolver = new Resolver(controllers, providers);
+ return resolver.resolve(token);
+}
-// decorators/method
-export { createHttpDecorator } from "./createHttpDecorator";
-export { createMethodDecorator } from "./createMethodDecorator";
-export type { AppsScriptOptions } from "./createMethodDecorator";
-
-// decorators/param
-export { assignMetadata } from "./assignMetadata";
-export { assignInjectMetadata } from "./assignInjectMetadata";
-export { createParamDecorator } from "./createParamDecorator";
+export function buildMethodParams(
+ target: object,
+ methodName: string | symbol,
+ context: { event: unknown },
+ controllers: Map,
+ providers: Map
+): unknown[] {
+ const resolver = new Resolver(controllers, providers);
+ const dispatcher = new EventDispatcher(resolver, controllers);
+ return dispatcher.buildMethodParams(target, methodName, context.event);
+}
diff --git a/src/utils/resolve.ts b/src/utils/resolve.ts
deleted file mode 100644
index 3fc1655..0000000
--- a/src/utils/resolve.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { isFunctionLike } from "apps-script-utils";
-import { PARAMTYPES_METADATA } from "../config/constants";
-import { Newable, ParamSource } from "../types";
-import { getInjectionTokens, isController, isInjectable } from "../utils";
-
-/**
- * Resolves a class (Controller or Provider) and its dependencies from the DI container.
- *
- * @template T - The type of the class to resolve.
- * @param controllers
- * @param providers
- * @param target - The constructor function of the class to be resolved.
- * This class should typically be decorated with `@Injectable()` or `@SheetsController()`.
- * @returns {T} An instance of the target class with all its dependencies injected.
- */
-export function resolve(
- controllers: Map,
- providers: Map,
- target: Newable
-): T {
- if (controllers.has(target)) {
- const instance = controllers.get(target);
-
- if (instance) {
- return instance as T;
- }
- }
-
- if (providers.has(target)) {
- const instance = providers.get(target);
-
- if (instance) {
- return instance as T;
- }
- }
-
- const designParamTypes: Newable[] =
- Reflect.getMetadata(PARAMTYPES_METADATA, target) || [];
-
- const explicitInjectTokens = getInjectionTokens(target);
-
- const deps = new Array(
- Math.max(designParamTypes.length, Object.keys(explicitInjectTokens).length)
- );
-
- for (let i = 0; i < deps.length; i++) {
- const paramKey = `${ParamSource.INJECT}:${i}`;
- const injectDefinition = explicitInjectTokens[paramKey];
- const tokenToResolve = injectDefinition
- ? injectDefinition.token
- : designParamTypes[i];
-
- if (!isFunctionLike(tokenToResolve)) {
- throw new Error(
- `[Resolve ERROR]: Invalid injection token at index ${i} of '${target.name}'. Expected a class constructor.`
- );
- }
-
- if (!providers.has(tokenToResolve) && !controllers.has(tokenToResolve)) {
- throw new Error(
- `[Resolve ERROR]: '${tokenToResolve.name}' is not registered as a provider or controller.`
- );
- }
-
- deps[i] = resolve(controllers, providers, tokenToResolve);
- }
-
- const instance = new target(...deps);
-
- if (isController(target)) {
- controllers.set(target, instance);
- } else if (isInjectable(target)) {
- providers.set(target, instance);
- } else {
- console.warn(
- `[Resolve WARN] ${target.name} is not registered as a provider and is not marked @Controller() or @Injectable().`
- );
- }
-
- return instance;
-}
diff --git a/test/decorators/class.test.ts b/test/decorators/class.test.ts
deleted file mode 100644
index 6e9e01b..0000000
--- a/test/decorators/class.test.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import "reflect-metadata";
-import { describe, expect, it } from "vitest";
-import {
- CONTROLLER_OPTIONS_METADATA,
- CONTROLLER_TYPE_METADATA,
- CONTROLLER_WATERMARK
-} from "../../src/config/constants";
-import { HttpController, RestController } from "../../src/decorators";
-
-describe("HttpController Decorator", () => {
- it("should define CONTROLLER_WATERMARK, CONTROLLER_TYPE_METADATA as 'http', and PATH_METADATA within options", () => {
- const testBasePath = "/api/users";
-
- @HttpController(testBasePath)
- class TestControllerA {}
-
- const isController = Reflect.getMetadata(
- CONTROLLER_WATERMARK,
- TestControllerA
- );
- expect(isController).toBe(true);
-
- const controllerType = Reflect.getMetadata(
- CONTROLLER_TYPE_METADATA,
- TestControllerA
- );
- expect(controllerType).toBe("http");
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerA
- );
- expect(controllerOptions).toEqual({ basePath: testBasePath });
- expect(controllerOptions.basePath).toBe(testBasePath);
- });
-
- it('should use "/" as the default base path if none is provided', () => {
- @HttpController()
- class TestControllerB {}
-
- const isController = Reflect.getMetadata(
- CONTROLLER_WATERMARK,
- TestControllerB
- );
- expect(isController).toBe(true);
-
- const controllerType = Reflect.getMetadata(
- CONTROLLER_TYPE_METADATA,
- TestControllerB
- );
- expect(controllerType).toBe("http");
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerB
- );
- expect(controllerOptions).toEqual({ basePath: "/" });
- expect(controllerOptions.basePath).toBe("/");
- });
-
- it("RestController should be an alias for HttpController and work the same way", () => {
- const aliasBasePath = "/products";
-
- @RestController(aliasBasePath)
- class TestControllerC {}
-
- const isController = Reflect.getMetadata(
- CONTROLLER_WATERMARK,
- TestControllerC
- );
- expect(isController).toBe(true);
-
- const controllerType = Reflect.getMetadata(
- CONTROLLER_TYPE_METADATA,
- TestControllerC
- );
- expect(controllerType).toBe("http");
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerC
- );
- expect(controllerOptions).toEqual({ basePath: aliasBasePath });
- expect(controllerOptions.basePath).toBe(aliasBasePath);
- });
-
- it('should handle undefined basePath by defaulting to "/"', () => {
- @HttpController(undefined)
- class TestControllerD {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerD
- );
- expect(controllerOptions).toEqual({ basePath: "/" });
- expect(controllerOptions.basePath).toBe("/");
- });
-
- it("should correctly handle an empty string as basePath", () => {
- const emptyBasePath = "";
-
- @HttpController(emptyBasePath)
- class TestControllerE {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerE
- );
- expect(controllerOptions).toEqual({ basePath: emptyBasePath });
- expect(controllerOptions.basePath).toBe(emptyBasePath);
- expect(controllerOptions.basePath).not.toBe("/");
- });
-
- it("should correctly handle a base path with a trailing slash", () => {
- const basePathWithTrailingSlash = "/admin/";
-
- @HttpController(basePathWithTrailingSlash)
- class TestControllerF {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerF
- );
- expect(controllerOptions).toEqual({ basePath: basePathWithTrailingSlash });
- expect(controllerOptions.basePath).toBe(basePathWithTrailingSlash);
- });
-
- it("should correctly handle a base path without a leading slash", () => {
- const basePathWithoutLeadingSlash = "dashboard";
-
- @HttpController(basePathWithoutLeadingSlash)
- class TestControllerG {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerG
- );
- expect(controllerOptions).toEqual({
- basePath: basePathWithoutLeadingSlash
- });
- expect(controllerOptions.basePath).toBe(basePathWithoutLeadingSlash);
- });
-
- it("should correctly handle a number as basePath (TypeScript will warn, but JS runtime allows)", () => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const numberBasePath = 123 as any;
-
- @HttpController(numberBasePath)
- class TestControllerH {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerH
- );
- expect(controllerOptions).toEqual({ basePath: numberBasePath });
- expect(controllerOptions.basePath).toBe(numberBasePath);
- expect(typeof controllerOptions.basePath).toBe("number");
- });
-
- it("should correctly handle null as basePath (TypeScript will warn, but JS runtime allows)", () => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const nullBasePath = null as any;
-
- @HttpController(nullBasePath)
- class TestControllerI {}
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- TestControllerI
- );
- expect(controllerOptions).toEqual({ basePath: nullBasePath });
- expect(controllerOptions.basePath).toBe(nullBasePath);
- expect(controllerOptions.basePath).toBeNull();
- });
-
- it("should not define any metadata if HttpController is not applied", () => {
- class NonDecoratedController {}
-
- const isController = Reflect.getMetadata(
- CONTROLLER_WATERMARK,
- NonDecoratedController
- );
- expect(isController).toBeUndefined();
-
- const controllerType = Reflect.getMetadata(
- CONTROLLER_TYPE_METADATA,
- NonDecoratedController
- );
- expect(controllerType).toBeUndefined();
-
- const controllerOptions = Reflect.getMetadata(
- CONTROLLER_OPTIONS_METADATA,
- NonDecoratedController
- );
- expect(controllerOptions).toBeUndefined();
- });
-});
diff --git a/test/decorators/method.test.ts b/test/decorators/method.test.ts
deleted file mode 100644
index 340200a..0000000
--- a/test/decorators/method.test.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import "reflect-metadata";
-import { describe, expect, it } from "vitest";
-import { METHOD_METADATA, PATH_METADATA } from "../../src/config/constants";
-import {
- Delete,
- DeleteMapping,
- Get,
- GetMapping,
- Head,
- HeadMapping,
- Options,
- OptionsMapping,
- Patch,
- PatchMapping,
- Post,
- PostMapping,
- Put,
- PutMapping
-} from "../../src/decorators";
-import { RequestMethod } from "../../src/types";
-
-describe("HTTP Method Decorators", () => {
- function testHttpMethodDecorator(
- decorator: (path?: string) => MethodDecorator,
- expectedMethod: RequestMethod,
- decoratorName: string
- ) {
- describe(`@${decoratorName}`, () => {
- it(`should define METHOD_METADATA and PATH_METADATA with a given path for ${decoratorName}`, () => {
- const testPath = "/custom-path";
-
- class TestClass {
- @decorator(testPath)
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedMethod = Reflect.getMetadata(
- METHOD_METADATA,
- methodFunction
- );
- expect(definedMethod).toBe(expectedMethod);
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe(testPath);
- });
-
- it(`should use "/" as the default path if none is provided for ${decoratorName}`, () => {
- class TestClass {
- @decorator()
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedMethod = Reflect.getMetadata(
- METHOD_METADATA,
- methodFunction
- );
- expect(definedMethod).toBe(expectedMethod);
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe("/");
- });
-
- it(`should handle undefined path by defaulting to "/" for ${decoratorName}`, () => {
- class TestClass {
- @decorator(undefined)
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedMethod = Reflect.getMetadata(
- METHOD_METADATA,
- methodFunction
- );
- expect(definedMethod).toBe(expectedMethod);
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe("/");
- });
-
- it(`should correctly handle an empty string as path for ${decoratorName}`, () => {
- const emptyPath = "";
-
- class TestClass {
- @decorator(emptyPath)
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe("/");
- });
-
- it(`should correctly handle a path with a trailing slash for ${decoratorName}`, () => {
- const trailingSlashPath = "/users/";
-
- class TestClass {
- @decorator(trailingSlashPath)
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe(trailingSlashPath);
- });
-
- it(`should correctly handle a path without a leading slash for ${decoratorName}`, () => {
- const noLeadingSlashPath = "users/profile";
-
- class TestClass {
- @decorator(noLeadingSlashPath)
- testMethod() {}
- }
-
- const methodFunction = TestClass.prototype.testMethod;
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBe(noLeadingSlashPath);
- });
-
- it("should not define metadata if the decorator is not applied to the method", () => {
- class TestClass {
- anotherMethod() {}
- }
-
- const methodFunction = TestClass.prototype.anotherMethod;
-
- const definedMethod = Reflect.getMetadata(
- METHOD_METADATA,
- methodFunction
- );
- expect(definedMethod).toBeUndefined();
-
- const definedPath = Reflect.getMetadata(PATH_METADATA, methodFunction);
- expect(definedPath).toBeUndefined();
- });
-
- it("should not interfere with metadata of other methods in the same class", () => {
- class TestClass {
- @decorator("/method1")
- method1() {}
-
- @Get("/method2")
- method2() {}
- }
-
- const method1Function = TestClass.prototype.method1;
- const method2Function = TestClass.prototype.method2;
-
- const method1Path = Reflect.getMetadata(PATH_METADATA, method1Function);
- const method1Method = Reflect.getMetadata(
- METHOD_METADATA,
- method1Function
- );
- expect(method1Path).toBe("/method1");
- expect(method1Method).toBe(expectedMethod);
-
- const method2Path = Reflect.getMetadata(PATH_METADATA, method2Function);
- const method2Method = Reflect.getMetadata(
- METHOD_METADATA,
- method2Function
- );
- expect(method2Path).toBe("/method2");
- expect(method2Method).toBe(RequestMethod.GET);
- });
- });
- }
-
- testHttpMethodDecorator(Get, RequestMethod.GET, "Get");
- testHttpMethodDecorator(Post, RequestMethod.POST, "Post");
- testHttpMethodDecorator(Put, RequestMethod.PUT, "Put");
- testHttpMethodDecorator(Delete, RequestMethod.DELETE, "Delete");
- testHttpMethodDecorator(Patch, RequestMethod.PATCH, "Patch");
- testHttpMethodDecorator(Options, RequestMethod.OPTIONS, "Options");
- testHttpMethodDecorator(Head, RequestMethod.HEAD, "Head");
-
- testHttpMethodDecorator(GetMapping, RequestMethod.GET, "GetMapping");
- testHttpMethodDecorator(PostMapping, RequestMethod.POST, "PostMapping");
- testHttpMethodDecorator(PutMapping, RequestMethod.PUT, "PutMapping");
- testHttpMethodDecorator(DeleteMapping, RequestMethod.DELETE, "DeleteMapping");
- testHttpMethodDecorator(PatchMapping, RequestMethod.PATCH, "PatchMapping");
- testHttpMethodDecorator(
- OptionsMapping,
- RequestMethod.OPTIONS,
- "OptionsMapping"
- );
- testHttpMethodDecorator(HeadMapping, RequestMethod.HEAD, "HeadMapping");
-});
diff --git a/test/decorators/param.test.ts b/test/decorators/param.test.ts
deleted file mode 100644
index 77a8bd2..0000000
--- a/test/decorators/param.test.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-import "reflect-metadata";
-import { describe, expect, it } from "vitest";
-import { PARAM_DEFINITIONS_METADATA } from "../../src/config/constants";
-import {
- Body,
- Event,
- Headers,
- Param,
- PathVariable,
- Query,
- Request,
- RequestBody,
- RequestParam,
- Response
-} from "../../src/decorators";
-import { ParamDefinition, ParamSource } from "../../src/types";
-
-function getParameterMetadata(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
- target: Function,
- propertyKey: string | symbol
-): Record {
- const metadataTarget = target.prototype;
-
- const rawMetadata: Record =
- Reflect.getMetadata(
- PARAM_DEFINITIONS_METADATA,
- metadataTarget,
- propertyKey
- ) || {};
-
- return rawMetadata;
-}
-
-describe("HTTP Parameter Decorators", () => {
- describe("@Param / @PathVariable", () => {
- it("should define correct metadata for a @Param with a key", () => {
- class TestController {
- testMethod(
- @Param("id")
- id: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
- });
- });
-
- it("should define correct metadata for a @Param without a key (full object injection)", () => {
- class TestController {
- testMethod(
- @Param()
- params: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: undefined, index: 0 }
- });
- });
-
- it("PathVariable should be an alias for Param", () => {
- class TestController {
- testMethod(
- @PathVariable("name")
- name: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: "name", index: 0 }
- });
- });
- });
-
- describe("@Query / @RequestParam", () => {
- it("should define correct metadata for a @Query with a key", () => {
- class TestController {
- testMethod(
- @Query("search")
- search: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: "search", index: 0 }
- });
- });
-
- it("should define correct metadata for a @Query without a key", () => {
- class TestController {
- testMethod(
- @Query()
- queryParams: never
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: undefined, index: 0 }
- });
- });
-
- it("RequestParam should be an alias for Query", () => {
- class TestController {
- testMethod(
- @RequestParam("sort")
- sort: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: "sort", index: 0 }
- });
- });
- });
-
- describe("@Body / @RequestBody", () => {
- it("should define correct metadata for a @Body with a key", () => {
- class TestController {
- testMethod(
- @Body("data")
- data: never
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "BODY:0": { type: ParamSource.BODY, key: "data", index: 0 }
- });
- });
-
- it("should define correct metadata for a @Body without a key (full body injection)", () => {
- class TestController {
- testMethod(
- @Body()
- fullBody: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "BODY:0": { type: ParamSource.BODY, key: undefined, index: 0 }
- });
- });
-
- it("RequestBody should be an alias for Body", () => {
- class TestController {
- testMethod(
- @RequestBody("user")
- user: never
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "BODY:0": { type: ParamSource.BODY, key: "user", index: 0 }
- });
- });
- });
-
- describe("@Event", () => {
- it("should define correct metadata for @Event", () => {
- class TestController {
- testMethod(
- @Event()
- event: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "EVENT:0": { type: ParamSource.EVENT, key: undefined, index: 0 }
- });
- });
-
- it("should define key as passed for @Event if provided", () => {
- class TestController {
- testMethod(
- @Event("someKey" as any)
- event: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "EVENT:0": { type: ParamSource.EVENT, key: "someKey", index: 0 }
- });
- });
- });
-
- describe("@Headers", () => {
- it("should define correct metadata for @Headers with a key", () => {
- class TestController {
- testMethod(
- @Headers("Authorization")
- auth: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "HEADERS:0": {
- type: ParamSource.HEADERS,
- key: "Authorization",
- index: 0
- }
- });
- });
-
- it("should define correct metadata for @Headers without a key (full headers object)", () => {
- class TestController {
- testMethod(
- @Headers()
- headers: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "HEADERS:0": { type: ParamSource.HEADERS, key: undefined, index: 0 }
- });
- });
- });
-
- describe("@Request", () => {
- it("should define correct metadata for @Request with a key", () => {
- class TestController {
- testMethod(
- @Request("method")
- method: string
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "REQUEST:0": { type: ParamSource.REQUEST, key: "method", index: 0 }
- });
- });
-
- it("should define correct metadata for @Request without a key (full request object)", () => {
- class TestController {
- testMethod(
- @Request()
- req: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "REQUEST:0": { type: ParamSource.REQUEST, key: undefined, index: 0 }
- });
- });
- });
-
- describe("@Response", () => {
- it("should define correct metadata for @Response with a key", () => {
- class TestController {
- testMethod(
- @Response("status")
- status: number
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "RESPONSE:0": { type: ParamSource.RESPONSE, key: "status", index: 0 }
- });
- });
-
- it("should define correct metadata for @Response without a key (full response object)", () => {
- class TestController {
- testMethod(
- @Response()
- res: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "RESPONSE:0": { type: ParamSource.RESPONSE, key: undefined, index: 0 }
- });
- });
- });
-
- describe("Multiple Parameters and Methods", () => {
- it("should define correct metadata for multiple parameters in the same method", () => {
- class TestController {
- testMethod(
- @Param("userId")
- id: string,
-
- @Query("search")
- search: string,
-
- @Body()
- body: any,
-
- @Event()
- event: any,
-
- @Headers("Accept")
- accept: string,
-
- @Request()
- req: any,
-
- @Response("ok")
- ok: boolean
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: "userId", index: 0 },
- "QUERY:1": { type: ParamSource.QUERY, key: "search", index: 1 },
- "BODY:2": { type: ParamSource.BODY, key: undefined, index: 2 },
- "EVENT:3": { type: ParamSource.EVENT, key: undefined, index: 3 },
- "HEADERS:4": { type: ParamSource.HEADERS, key: "Accept", index: 4 },
- "REQUEST:5": { type: ParamSource.REQUEST, key: undefined, index: 5 },
- "RESPONSE:6": { type: ParamSource.RESPONSE, key: "ok", index: 6 }
- });
- });
-
- it("should define correct metadata for parameters in different methods", () => {
- class TestController {
- methodOne(
- @Param("id")
- id: string
- ) {}
-
- methodTwo(
- @Query("page")
- page: number
- ) {}
- }
-
- const metadataOne = getParameterMetadata(TestController, "methodOne");
- expect(metadataOne).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
- });
-
- const metadataTwo = getParameterMetadata(TestController, "methodTwo");
- expect(metadataTwo).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: "page", index: 0 }
- });
-
- expect(metadataOne).not.toHaveProperty("QUERY:0");
- expect(metadataTwo).not.toHaveProperty("PARAM:0");
- });
-
- it("should correctly handle parameters with the same index but different methods", () => {
- class TestController {
- methodA(
- @Param("alpha")
- alpha: string
- ) {}
-
- methodB(
- @Query("beta")
- beta: string
- ) {}
- }
-
- const metadataA = getParameterMetadata(TestController, "methodA");
- expect(metadataA).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: "alpha", index: 0 }
- });
-
- const metadataB = getParameterMetadata(TestController, "methodB");
- expect(metadataB).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: "beta", index: 0 }
- });
- });
- });
-
- describe("No Decorator Applied", () => {
- it("should not define any parameter metadata if no decorator is applied to a method", () => {
- class TestController {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- someMethod(arg1: string, arg2: number) {}
- }
-
- const metadata = getParameterMetadata(TestController, "someMethod");
- expect(metadata).toEqual({}); // Метаданных не должно быть
- });
-
- it("should not define any parameter metadata for a method that does not exist", () => {
- class TestController {
- existingMethod() {}
- }
-
- const metadata = getParameterMetadata(
- TestController,
- "nonExistentMethod"
- );
- expect(metadata).toEqual({});
- });
- });
-
- describe("Invalid/Unexpected Keys", () => {
- it("should store null as key if provided", () => {
- class TestController {
- testMethod(
- @Param(null as any)
- param: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "PARAM:0": { type: ParamSource.PARAM, key: null, index: 0 }
- });
- });
-
- it("should store empty string as key if provided", () => {
- class TestController {
- testMethod(
- @Query("")
- query: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "QUERY:0": { type: ParamSource.QUERY, key: "", index: 0 }
- });
- });
-
- it("should store number as key if provided (TS warning, but JS allows)", () => {
- class TestController {
- testMethod(
- @Body(123 as any)
- body: any
- ) {}
- }
-
- const metadata = getParameterMetadata(TestController, "testMethod");
- expect(metadata).toEqual({
- "BODY:0": { type: ParamSource.BODY, key: 123, index: 0 }
- });
- });
- });
-});
diff --git a/test/integration/BootApplication/BootApplication.boundary.integration.test.ts b/test/integration/BootApplication/BootApplication.boundary.integration.test.ts
new file mode 100644
index 0000000..f0de4e2
--- /dev/null
+++ b/test/integration/BootApplication/BootApplication.boundary.integration.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+
+describe("Integration: BootApplication: Boundary", () => {
+ const app = BootApplicationFactory.create({
+ controllers: []
+ });
+
+ (global as unknown as Record).HtmlService = {
+ createHtmlOutput: vi.fn().mockReturnValue({})
+ };
+
+ it("should handle empty event without crashing", async () => {
+ const event = {} as unknown as GoogleAppsScript.Events.DoGet;
+ await expect(app.doGet(event)).resolves.toBeDefined();
+ });
+});
diff --git a/test/integration/BootApplication/BootApplication.negative.integration.test.ts b/test/integration/BootApplication/BootApplication.negative.integration.test.ts
new file mode 100644
index 0000000..cba5a7a
--- /dev/null
+++ b/test/integration/BootApplication/BootApplication.negative.integration.test.ts
@@ -0,0 +1,38 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+import { Get } from "src/controller/decorators/routing";
+import { HttpController } from "src/controller/decorators";
+
+@HttpController("/users")
+class UserController {
+ @Get("/")
+ getAll() {
+ return [];
+ }
+}
+
+describe("Integration: BootApplication: Negative", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [UserController]
+ });
+
+ (global as unknown as Record).HtmlService = {
+ createHtmlOutput: vi.fn().mockReturnValue({})
+ };
+
+ it("should return 404 for unknown route", async () => {
+ const event = {
+ pathInfo: "unknown",
+ parameter: {},
+ parameters: {}
+ } as unknown as GoogleAppsScript.Events.DoGet;
+
+ await app.doGet(event);
+
+ expect(global.HtmlService.createHtmlOutput).toHaveBeenCalled();
+ const callArgs = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls[0][0];
+ const responseBody = JSON.parse(callArgs as string);
+ expect(responseBody).toEqual({ error: { message: "Cannot get /unknown" } });
+ });
+});
diff --git a/test/integration/BootApplication/BootApplication.positive.integration.test.ts b/test/integration/BootApplication/BootApplication.positive.integration.test.ts
new file mode 100644
index 0000000..22d2bda
--- /dev/null
+++ b/test/integration/BootApplication/BootApplication.positive.integration.test.ts
@@ -0,0 +1,77 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { Body, Param, Query } from "src/controller/decorators/params";
+import { BootApplicationFactory } from "src/controller";
+import { Get, Post } from "src/controller/decorators/routing";
+import { HttpController } from "src/controller/decorators";
+
+@HttpController("/users")
+class UserController {
+ @Get("/{id}")
+ getUser(@Param("id") id: string, @Query("fields") fields: string) {
+ return { id, fields, method: "GET" };
+ }
+
+ @Post("/")
+ createUser(@Body() data: Record) {
+ return { ...data, method: "POST" };
+ }
+}
+
+describe("Integration: BootApplication: Positive", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [UserController]
+ });
+
+ // Mock ContentService and HtmlService
+ (global as unknown as Record).ContentService = {
+ createTextOutput: vi.fn().mockReturnValue({
+ setMimeType: vi.fn().mockReturnThis()
+ }),
+ MimeType: {
+ JSON: "JSON",
+ TEXT: "TEXT"
+ }
+ };
+
+ (global as unknown as Record).HtmlService = {
+ createHtmlOutput: vi.fn().mockReturnValue({})
+ };
+
+ it("should handle GET request with path and query params", async () => {
+ const event = {
+ parameter: { fields: "name,email" },
+ parameters: { fields: ["name,email"] },
+ contextPath: "",
+ contentLength: -1,
+ queryString: "fields=name,email",
+ pathInfo: "users/123"
+ } as unknown as GoogleAppsScript.Events.DoGet;
+
+ await app.doGet(event);
+
+ expect(global.HtmlService.createHtmlOutput).toHaveBeenCalled();
+ const callArgs = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls[0][0];
+ const responseBody = JSON.parse(callArgs as string);
+ expect(responseBody).toEqual({ id: "123", fields: ["name,email"], method: "GET" });
+ });
+
+ it("should handle POST request with body", async () => {
+ const data = { name: "John Doe" };
+ const event = {
+ postData: {
+ contents: JSON.stringify(data),
+ type: "application/json"
+ },
+ pathInfo: "users"
+ } as unknown as GoogleAppsScript.Events.DoPost;
+
+ await app.doPost(event);
+
+ expect(global.HtmlService.createHtmlOutput).toHaveBeenCalled();
+ const lastCall = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls.length - 1;
+ const callArgs = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls[lastCall][0];
+ const responseBody = JSON.parse(callArgs as string);
+ expect(responseBody).toEqual({ name: "John Doe", method: "POST" });
+ });
+});
diff --git a/test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts b/test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts
new file mode 100644
index 0000000..1fc374d
--- /dev/null
+++ b/test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { BootApplication } from "src/controller";
+import { AppsScriptEventType } from "src/domain/enums";
+
+describe("BootApplication: Positive", () => {
+ it("should support different types of providers during initialization", () => {
+ class ClassProvider {}
+ const valueProvider = { provide: "TOKEN", useValue: "VALUE" };
+ const classProvider = { provide: "SERVICE", useClass: class {} };
+ const factoryProvider = { provide: "FACTORY", useFactory: () => ({}) };
+ const existingProvider = { provide: "EXISTING", useExisting: "OTHER" };
+
+ const app = new BootApplication({
+ controllers: [],
+ providers: [ClassProvider, valueProvider, classProvider, factoryProvider, existingProvider]
+ });
+
+ const providers = (app as unknown as { _providers: Map })._providers;
+ expect(providers.has(ClassProvider)).toBe(true);
+ expect(providers.get("TOKEN")).toBe("VALUE");
+ expect(providers.has("SERVICE")).toBe(true);
+ expect(providers.has("FACTORY")).toBe(true);
+ expect(providers.has("EXISTING")).toBe(true);
+ });
+
+ it("should dispatch INSTALL event on onInstall", async () => {
+ const app = new BootApplication({ controllers: [] });
+ const dispatchSpy = vi
+ .spyOn(
+ (app as unknown as { _eventDispatcher: { dispatch: () => Promise } })
+ ._eventDispatcher,
+ "dispatch"
+ )
+ .mockResolvedValue(undefined);
+
+ const event = {} as GoogleAppsScript.Events.AddonOnInstall;
+ await app.onInstall(event);
+
+ expect(dispatchSpy).toHaveBeenCalledWith(AppsScriptEventType.INSTALL, event);
+ });
+
+ it("should dispatch FORM_SUBMIT event on onFormSubmit", async () => {
+ const app = new BootApplication({ controllers: [] });
+ const dispatchSpy = vi
+ .spyOn(
+ (app as unknown as { _eventDispatcher: { dispatch: () => Promise } })
+ ._eventDispatcher,
+ "dispatch"
+ )
+ .mockResolvedValue(undefined);
+
+ const event = {} as GoogleAppsScript.Events.FormsOnFormSubmit;
+ await app.onFormSubmit(event);
+
+ expect(dispatchSpy).toHaveBeenCalledWith(AppsScriptEventType.FORM_SUBMIT, event);
+ });
+});
diff --git a/test/unit/controller/BootApplicationFactory/BootApplicationFactory.positive.unit.test.ts b/test/unit/controller/BootApplicationFactory/BootApplicationFactory.positive.unit.test.ts
new file mode 100644
index 0000000..b443f3d
--- /dev/null
+++ b/test/unit/controller/BootApplicationFactory/BootApplicationFactory.positive.unit.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+import { BootApplication } from "src/controller";
+
+describe("BootApplicationFactory: Positive", () => {
+ it("should create a BootApplication instance", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [],
+ providers: []
+ });
+ expect(app).toBeInstanceOf(BootApplication);
+ });
+});
diff --git a/test/unit/controller/decorators/Controller/Controller.negative.unit.test.ts b/test/unit/controller/decorators/Controller/Controller.negative.unit.test.ts
new file mode 100644
index 0000000..44f09eb
--- /dev/null
+++ b/test/unit/controller/decorators/Controller/Controller.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { CONTROLLER_WATERMARK } from "src/domain/constants";
+
+describe("@Controller: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts b/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts
new file mode 100644
index 0000000..d6ed849
--- /dev/null
+++ b/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts
@@ -0,0 +1,29 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "src/domain/constants";
+import { Controller } from "src/controller/decorators";
+
+describe("@Controller: Positive", () => {
+ it("should define correct metadata", () => {
+ const type = "custom";
+ const options = { basePath: "/custom" };
+
+ @Controller(type, options)
+ class TestController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestController)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_TYPE_METADATA, TestController)).toBe(type);
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestController)).toEqual(options);
+ });
+
+ it("should use empty options by default", () => {
+ @Controller("simple")
+ class TestController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestController)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/Decorators.extra.unit.test.ts b/test/unit/controller/decorators/Decorators.extra.unit.test.ts
new file mode 100644
index 0000000..310704f
--- /dev/null
+++ b/test/unit/controller/decorators/Decorators.extra.unit.test.ts
@@ -0,0 +1,50 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { Param } from "src/controller/decorators/params";
+import { Inject } from "src/controller/decorators";
+import { createHttpDecorator } from "src/repository";
+import {
+ INJECT_TOKENS_METADATA,
+ METHOD_METADATA,
+ PARAM_DEFINITIONS_METADATA
+} from "src/domain/constants";
+import { RequestMethod } from "src/domain/enums";
+
+describe("Decorators: Extra Coverage", () => {
+ it("should cover Inject in methods and constructors", () => {
+ class Test {
+ constructor(@Inject("BASE") _base: unknown) {}
+ method(@Inject("METHOD") _m: unknown) {}
+ }
+
+ const ctorMeta = Reflect.getMetadata(INJECT_TOKENS_METADATA, Test);
+ expect(ctorMeta).toBeDefined();
+
+ const methodMeta = Reflect.getMetadata(INJECT_TOKENS_METADATA, Test.prototype, "method");
+ expect(methodMeta).toBeDefined();
+ });
+
+ it("should cover Param in methods and constructors", () => {
+ class Test {
+ constructor(@Param("id") __id: string) {}
+ method(@Param("name") __name: string) {}
+ }
+
+ const ctorMeta = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test);
+ expect(ctorMeta).toBeDefined();
+
+ const methodMeta = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method");
+ expect(methodMeta).toBeDefined();
+ });
+
+ it("should cover createHttpDecorator default method", () => {
+ const CustomGet = createHttpDecorator(undefined as unknown as RequestMethod);
+ class Test {
+ @CustomGet("/")
+ method() {}
+ }
+
+ const method = Test.prototype.method;
+ expect(Reflect.getMetadata(METHOD_METADATA, method)).toBe(RequestMethod.GET);
+ });
+});
diff --git a/test/unit/controller/decorators/Entity/Entity.boundary.unit.test.ts b/test/unit/controller/decorators/Entity/Entity.boundary.unit.test.ts
new file mode 100644
index 0000000..75366d4
--- /dev/null
+++ b/test/unit/controller/decorators/Entity/Entity.boundary.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { ENTITY_WATERMARK } from "src/domain/constants";
+import { Entity } from "src/controller/decorators";
+
+describe("@Entity: Boundary", () => {
+ it("should handle multiple @Entity decorators", () => {
+ @Entity()
+ @Entity()
+ class TestClass {}
+
+ expect(Reflect.getMetadata(ENTITY_WATERMARK, TestClass)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/Entity/Entity.negative.unit.test.ts b/test/unit/controller/decorators/Entity/Entity.negative.unit.test.ts
new file mode 100644
index 0000000..58c6e07
--- /dev/null
+++ b/test/unit/controller/decorators/Entity/Entity.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { ENTITY_WATERMARK } from "src/domain/constants";
+
+describe("@Entity: Negative", () => {
+ it("should not define watermark if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(ENTITY_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/Entity/Entity.positive.unit.test.ts b/test/unit/controller/decorators/Entity/Entity.positive.unit.test.ts
new file mode 100644
index 0000000..36d53ca
--- /dev/null
+++ b/test/unit/controller/decorators/Entity/Entity.positive.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { ENTITY_WATERMARK } from "src/domain/constants";
+import { Entity } from "src/controller/decorators";
+
+describe("@Entity: Positive", () => {
+ it("should define ENTITY_WATERMARK", () => {
+ @Entity()
+ class TestEntity {}
+
+ expect(Reflect.getMetadata(ENTITY_WATERMARK, TestEntity)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/HttpController/HttpController.boundary.unit.test.ts b/test/unit/controller/decorators/HttpController/HttpController.boundary.unit.test.ts
new file mode 100644
index 0000000..953dfe4
--- /dev/null
+++ b/test/unit/controller/decorators/HttpController/HttpController.boundary.unit.test.ts
@@ -0,0 +1,41 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { CONTROLLER_OPTIONS_METADATA } from "src/domain/constants";
+import { HttpController } from "src/controller/decorators";
+
+describe("HttpController Decorator: Boundary", () => {
+ it('should handle undefined basePath by defaulting to "/"', () => {
+ @HttpController(undefined)
+ class TestControllerD {}
+
+ const controllerOptions = Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerD);
+ expect(controllerOptions).toEqual({ basePath: "/" });
+ });
+
+ it("should correctly handle an empty string as basePath", () => {
+ const emptyBasePath = "";
+ @HttpController(emptyBasePath)
+ class TestControllerE {}
+
+ const controllerOptions = Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerE);
+ expect(controllerOptions).toEqual({ basePath: emptyBasePath });
+ });
+
+ it("should correctly handle a number as basePath", () => {
+ const numberBasePath = 123 as unknown as string;
+ @HttpController(numberBasePath)
+ class TestControllerH {}
+
+ const controllerOptions = Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerH);
+ expect(controllerOptions).toEqual({ basePath: numberBasePath });
+ });
+
+ it("should correctly handle null as basePath", () => {
+ const nullBasePath = null as unknown as string;
+ @HttpController(nullBasePath)
+ class TestControllerI {}
+
+ const controllerOptions = Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerI);
+ expect(controllerOptions).toEqual({ basePath: nullBasePath });
+ });
+});
diff --git a/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts b/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts
new file mode 100644
index 0000000..8ffe1fb
--- /dev/null
+++ b/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts
@@ -0,0 +1,19 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "src/domain/constants";
+
+describe("HttpController Decorator: Negative", () => {
+ it("should not define any metadata if HttpController is not applied", () => {
+ class NonDecoratedController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, NonDecoratedController)).toBeUndefined();
+ expect(Reflect.getMetadata(CONTROLLER_TYPE_METADATA, NonDecoratedController)).toBeUndefined();
+ expect(
+ Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, NonDecoratedController)
+ ).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts b/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts
new file mode 100644
index 0000000..dc368f6
--- /dev/null
+++ b/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts
@@ -0,0 +1,63 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "src/domain/constants";
+import { HttpController, RestController } from "src/controller/decorators";
+
+describe("HttpController Decorator: Positive", () => {
+ it("should define CONTROLLER_WATERMARK, CONTROLLER_TYPE_METADATA as 'http', and PATH_METADATA within options", () => {
+ const testBasePath = "/api/users";
+ @HttpController(testBasePath)
+ class TestControllerA {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestControllerA)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_TYPE_METADATA, TestControllerA)).toBe("http");
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerA)).toEqual({
+ basePath: testBasePath
+ });
+ });
+
+ it('should use "/" as the default base path if none is provided', () => {
+ @HttpController()
+ class TestControllerB {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestControllerB)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerB)).toEqual({
+ basePath: "/"
+ });
+ });
+
+ it("RestController should be an alias for HttpController", () => {
+ const aliasBasePath = "/products";
+ @RestController(aliasBasePath)
+ class TestControllerC {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestControllerC)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerC)).toEqual({
+ basePath: aliasBasePath
+ });
+ });
+
+ it("should correctly handle a base path with a trailing slash", () => {
+ const basePathWithTrailingSlash = "/admin/";
+ @HttpController(basePathWithTrailingSlash)
+ class TestControllerF {}
+
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerF)).toEqual({
+ basePath: basePathWithTrailingSlash
+ });
+ });
+
+ it("should correctly handle a base path without a leading slash", () => {
+ const basePathWithoutLeadingSlash = "dashboard";
+ @HttpController(basePathWithoutLeadingSlash)
+ class TestControllerG {}
+
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestControllerG)).toEqual({
+ basePath: basePathWithoutLeadingSlash
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts
new file mode 100644
index 0000000..86148d2
--- /dev/null
+++ b/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+import { Resolver } from "src/service";
+import { HttpController, Inject } from "src/controller/decorators";
+
+const STRING_TOKEN = "STRING_TOKEN";
+
+@HttpController()
+class StringTokenController {
+ constructor(@Inject(STRING_TOKEN) public value: string) {}
+}
+
+describe("Inject Decorator: Boundary", () => {
+ it("should correctly inject dependency using a string token", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [StringTokenController],
+ providers: [{ provide: STRING_TOKEN, useValue: "token_value" }]
+ });
+
+ const instance = (app as unknown as { _resolver: Resolver })._resolver.resolve(
+ StringTokenController
+ );
+ expect(instance.value).toBe("token_value");
+ });
+});
diff --git a/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts
new file mode 100644
index 0000000..26844ea
--- /dev/null
+++ b/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+import { Resolver } from "src/service";
+import { HttpController, Inject, Injectable } from "src/controller/decorators";
+
+@Injectable()
+class UnregisteredService {}
+
+@HttpController()
+class FaultyController {
+ constructor(@Inject(UnregisteredService) public service: UnregisteredService) {}
+}
+
+describe("Inject Decorator: Negative", () => {
+ it("should throw error when injecting an unregistered provider", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [FaultyController],
+ providers: []
+ });
+
+ expect(() =>
+ (app as unknown as { _resolver: Resolver })._resolver.resolve(FaultyController)
+ ).toThrow(/'UnregisteredService' is not registered/);
+ });
+});
diff --git a/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts
new file mode 100644
index 0000000..3c68ff5
--- /dev/null
+++ b/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts
@@ -0,0 +1,47 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { BootApplicationFactory } from "src/controller";
+import { Resolver } from "src/service";
+import { Get } from "src/controller/decorators/routing";
+import { HttpController, Inject, Injectable } from "src/controller/decorators";
+
+@Injectable()
+class ServiceA {
+ getValue() {
+ return "A";
+ }
+}
+
+@Injectable()
+class ServiceB {
+ getValue() {
+ return "B";
+ }
+}
+
+@HttpController()
+class TestController {
+ constructor(
+ @Inject(ServiceA) public serviceA: ServiceA,
+ @Inject(ServiceB) public serviceB: ServiceB
+ ) {}
+
+ @Get("/test")
+ test() {
+ return this.serviceA.getValue() + this.serviceB.getValue();
+ }
+}
+
+describe("Inject Decorator: Positive", () => {
+ it("should correctly inject dependencies using constructor and @Inject", () => {
+ const app = BootApplicationFactory.create({
+ controllers: [TestController],
+ providers: [ServiceA, ServiceB]
+ });
+
+ const instance = (app as unknown as { _resolver: Resolver })._resolver.resolve(TestController);
+ expect(instance.serviceA).toBeInstanceOf(ServiceA);
+ expect(instance.serviceB).toBeInstanceOf(ServiceB);
+ expect(instance.test()).toBe("AB");
+ });
+});
diff --git a/test/unit/controller/decorators/Injectable/Injectable.boundary.unit.test.ts b/test/unit/controller/decorators/Injectable/Injectable.boundary.unit.test.ts
new file mode 100644
index 0000000..9eb987b
--- /dev/null
+++ b/test/unit/controller/decorators/Injectable/Injectable.boundary.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Injectable } from "src/controller/decorators";
+
+describe("@Injectable: Boundary", () => {
+ it("should handle multiple @Injectable decorators", () => {
+ @Injectable()
+ @Injectable()
+ class TestClass {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/Injectable/Injectable.negative.unit.test.ts b/test/unit/controller/decorators/Injectable/Injectable.negative.unit.test.ts
new file mode 100644
index 0000000..8ee0274
--- /dev/null
+++ b/test/unit/controller/decorators/Injectable/Injectable.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+
+describe("@Injectable: Negative", () => {
+ it("should not define watermark if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/Injectable/Injectable.positive.unit.test.ts b/test/unit/controller/decorators/Injectable/Injectable.positive.unit.test.ts
new file mode 100644
index 0000000..3e074d2
--- /dev/null
+++ b/test/unit/controller/decorators/Injectable/Injectable.positive.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Injectable } from "src/controller/decorators";
+
+describe("@Injectable: Positive", () => {
+ it("should define INJECTABLE_WATERMARK", () => {
+ @Injectable()
+ class TestService {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestService)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/Repository/Repository.boundary.unit.test.ts b/test/unit/controller/decorators/Repository/Repository.boundary.unit.test.ts
new file mode 100644
index 0000000..f1a3d66
--- /dev/null
+++ b/test/unit/controller/decorators/Repository/Repository.boundary.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Repository } from "src/controller/decorators";
+
+describe("@Repository: Boundary", () => {
+ it("should handle multiple @Repository decorators", () => {
+ @Repository()
+ @Repository()
+ class TestClass {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/Repository/Repository.negative.unit.test.ts b/test/unit/controller/decorators/Repository/Repository.negative.unit.test.ts
new file mode 100644
index 0000000..dc47566
--- /dev/null
+++ b/test/unit/controller/decorators/Repository/Repository.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+
+describe("@Repository: Negative", () => {
+ it("should not define watermark if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/Repository/Repository.positive.unit.test.ts b/test/unit/controller/decorators/Repository/Repository.positive.unit.test.ts
new file mode 100644
index 0000000..c0de5c8
--- /dev/null
+++ b/test/unit/controller/decorators/Repository/Repository.positive.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Repository } from "src/controller/decorators";
+
+describe("@Repository: Positive", () => {
+ it("should define INJECTABLE_WATERMARK", () => {
+ @Repository()
+ class TestRepo {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestRepo)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts b/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts
new file mode 100644
index 0000000..5b19411
--- /dev/null
+++ b/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "src/domain/constants";
+import { RestController } from "src/controller/decorators";
+
+describe("@RestController: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ @RestController("/first")
+ @RestController("/second")
+ class TestController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestController)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_TYPE_METADATA, TestController)).toBe("http");
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestController)).toEqual({
+ basePath: "/first"
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/RestController/RestController.negative.unit.test.ts b/test/unit/controller/decorators/RestController/RestController.negative.unit.test.ts
new file mode 100644
index 0000000..0a95f14
--- /dev/null
+++ b/test/unit/controller/decorators/RestController/RestController.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { CONTROLLER_WATERMARK } from "src/domain/constants";
+
+describe("@RestController: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts b/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts
new file mode 100644
index 0000000..6077da2
--- /dev/null
+++ b/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts
@@ -0,0 +1,30 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import {
+ CONTROLLER_OPTIONS_METADATA,
+ CONTROLLER_TYPE_METADATA,
+ CONTROLLER_WATERMARK
+} from "src/domain/constants";
+import { RestController } from "src/controller/decorators";
+
+describe("@RestController: Positive", () => {
+ it("should define correct metadata", () => {
+ const basePath = "/api";
+
+ @RestController(basePath)
+ class TestController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_WATERMARK, TestController)).toBe(true);
+ expect(Reflect.getMetadata(CONTROLLER_TYPE_METADATA, TestController)).toBe("http");
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestController)).toEqual({ basePath });
+ });
+
+ it('should use "/" as default base path', () => {
+ @RestController()
+ class TestController {}
+
+ expect(Reflect.getMetadata(CONTROLLER_OPTIONS_METADATA, TestController)).toEqual({
+ basePath: "/"
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/Service/Service.boundary.unit.test.ts b/test/unit/controller/decorators/Service/Service.boundary.unit.test.ts
new file mode 100644
index 0000000..8de2779
--- /dev/null
+++ b/test/unit/controller/decorators/Service/Service.boundary.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Service } from "src/controller/decorators";
+
+describe("@Service: Boundary", () => {
+ it("should handle multiple @Service decorators", () => {
+ @Service()
+ @Service()
+ class TestClass {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/Service/Service.negative.unit.test.ts b/test/unit/controller/decorators/Service/Service.negative.unit.test.ts
new file mode 100644
index 0000000..6527a68
--- /dev/null
+++ b/test/unit/controller/decorators/Service/Service.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+
+describe("@Service: Negative", () => {
+ it("should not define watermark if decorator is not applied", () => {
+ class TestClass {}
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestClass)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/Service/Service.positive.unit.test.ts b/test/unit/controller/decorators/Service/Service.positive.unit.test.ts
new file mode 100644
index 0000000..e95cb9a
--- /dev/null
+++ b/test/unit/controller/decorators/Service/Service.positive.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { INJECTABLE_WATERMARK } from "src/domain/constants";
+import { Service } from "src/controller/decorators";
+
+describe("@Service: Positive", () => {
+ it("should define INJECTABLE_WATERMARK", () => {
+ @Service()
+ class TestService {}
+
+ expect(Reflect.getMetadata(INJECTABLE_WATERMARK, TestService)).toBe(true);
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnChange/OnChange.boundary.unit.test.ts b/test/unit/controller/decorators/appsscript/OnChange/OnChange.boundary.unit.test.ts
new file mode 100644
index 0000000..0e4f0a0
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnChange/OnChange.boundary.unit.test.ts
@@ -0,0 +1,20 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnChange } from "src/controller/decorators/appsscript";
+
+describe("@OnChange: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OnChange({ id: 1 })
+ @OnChange({ id: 2 })
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.CHANGE
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({ id: 1 });
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnChange/OnChange.negative.unit.test.ts b/test/unit/controller/decorators/appsscript/OnChange/OnChange.negative.unit.test.ts
new file mode 100644
index 0000000..271097b
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnChange/OnChange.negative.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA } from "src/domain/constants";
+
+describe("@OnChange: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnChange/OnChange.positive.unit.test.ts b/test/unit/controller/decorators/appsscript/OnChange/OnChange.positive.unit.test.ts
new file mode 100644
index 0000000..e8e3f2d
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnChange/OnChange.positive.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnChange } from "src/controller/decorators/appsscript";
+
+describe("@OnChange: Positive", () => {
+ it("should define APPSSCRIPT_EVENT_METADATA and APPSSCRIPT_OPTIONS_METADATA", () => {
+ const options = { some: "option" };
+ class TestClass {
+ @OnChange(options)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.CHANGE
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual(options);
+ });
+
+ it("should use empty object as default options", () => {
+ class TestClass {
+ @OnChange()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.CHANGE
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.boundary.unit.test.ts b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.boundary.unit.test.ts
new file mode 100644
index 0000000..1fd740a
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.boundary.unit.test.ts
@@ -0,0 +1,20 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnEdit } from "src/controller/decorators/appsscript";
+
+describe("@OnEdit: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OnEdit({ id: 1 })
+ @OnEdit({ id: 2 })
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.EDIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({ id: 1 });
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.negative.unit.test.ts b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.negative.unit.test.ts
new file mode 100644
index 0000000..99eb22a
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.negative.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA } from "src/domain/constants";
+
+describe("@OnEdit: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.positive.unit.test.ts b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.positive.unit.test.ts
new file mode 100644
index 0000000..aacc4af
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnEdit/OnEdit.positive.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnEdit } from "src/controller/decorators/appsscript";
+
+describe("@OnEdit: Positive", () => {
+ it("should define APPSSCRIPT_EVENT_METADATA and APPSSCRIPT_OPTIONS_METADATA", () => {
+ const options = { some: "option" };
+ class TestClass {
+ @OnEdit(options)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.EDIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual(options);
+ });
+
+ it("should use empty object as default options", () => {
+ class TestClass {
+ @OnEdit()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.EDIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.boundary.unit.test.ts b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.boundary.unit.test.ts
new file mode 100644
index 0000000..c0295e2
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.boundary.unit.test.ts
@@ -0,0 +1,20 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnFormSubmit } from "src/controller/decorators/appsscript";
+
+describe("@OnFormSubmit: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OnFormSubmit({ id: 1 })
+ @OnFormSubmit({ id: 2 })
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.FORM_SUBMIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({ id: 1 });
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.negative.unit.test.ts b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.negative.unit.test.ts
new file mode 100644
index 0000000..641d196
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.negative.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA } from "src/domain/constants";
+
+describe("@OnFormSubmit: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.positive.unit.test.ts b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.positive.unit.test.ts
new file mode 100644
index 0000000..f66dcde
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.positive.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnFormSubmit } from "src/controller/decorators/appsscript";
+
+describe("@OnFormSubmit: Positive", () => {
+ it("should define APPSSCRIPT_EVENT_METADATA and APPSSCRIPT_OPTIONS_METADATA", () => {
+ const options = { some: "option" };
+ class TestClass {
+ @OnFormSubmit(options)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.FORM_SUBMIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual(options);
+ });
+
+ it("should use empty object as default options", () => {
+ class TestClass {
+ @OnFormSubmit()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.FORM_SUBMIT
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.boundary.unit.test.ts b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.boundary.unit.test.ts
new file mode 100644
index 0000000..65cb301
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.boundary.unit.test.ts
@@ -0,0 +1,20 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnInstall } from "src/controller/decorators/appsscript";
+
+describe("@OnInstall: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OnInstall({ id: 1 })
+ @OnInstall({ id: 2 })
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.INSTALL
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({ id: 1 });
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.negative.unit.test.ts b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.negative.unit.test.ts
new file mode 100644
index 0000000..75ebfef
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.negative.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA } from "src/domain/constants";
+
+describe("@OnInstall: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.positive.unit.test.ts b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.positive.unit.test.ts
new file mode 100644
index 0000000..8ab7c25
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnInstall/OnInstall.positive.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnInstall } from "src/controller/decorators/appsscript";
+
+describe("@OnInstall: Positive", () => {
+ it("should define APPSSCRIPT_EVENT_METADATA and APPSSCRIPT_OPTIONS_METADATA", () => {
+ const options = { some: "option" };
+ class TestClass {
+ @OnInstall(options)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.INSTALL
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual(options);
+ });
+
+ it("should use empty object as default options", () => {
+ class TestClass {
+ @OnInstall()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.INSTALL
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.boundary.unit.test.ts b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.boundary.unit.test.ts
new file mode 100644
index 0000000..3ccd024
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.boundary.unit.test.ts
@@ -0,0 +1,20 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnOpen } from "src/controller/decorators/appsscript";
+
+describe("@OnOpen: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OnOpen({ id: 1 })
+ @OnOpen({ id: 2 })
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.OPEN
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({ id: 1 });
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.negative.unit.test.ts b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.negative.unit.test.ts
new file mode 100644
index 0000000..3642111
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.negative.unit.test.ts
@@ -0,0 +1,13 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA } from "src/domain/constants";
+
+describe("@OnOpen: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.positive.unit.test.ts b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.positive.unit.test.ts
new file mode 100644
index 0000000..e80b5ad
--- /dev/null
+++ b/test/unit/controller/decorators/appsscript/OnOpen/OnOpen.positive.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+import { OnOpen } from "src/controller/decorators/appsscript";
+
+describe("@OnOpen: Positive", () => {
+ it("should define APPSSCRIPT_EVENT_METADATA and APPSSCRIPT_OPTIONS_METADATA", () => {
+ const options = { some: "option" };
+ class TestClass {
+ @OnOpen(options)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.OPEN
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual(options);
+ });
+
+ it("should use empty object as default options", () => {
+ class TestClass {
+ @OnOpen()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodFunction)).toBe(
+ AppsScriptEventType.OPEN
+ );
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, methodFunction)).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Body/Body.boundary.unit.test.ts b/test/unit/controller/decorators/params/Body/Body.boundary.unit.test.ts
new file mode 100644
index 0000000..8bb0546
--- /dev/null
+++ b/test/unit/controller/decorators/params/Body/Body.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Body } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Body: Boundary", () => {
+ it("should store number as key if provided (TS warning, but JS allows)", () => {
+ class TestController {
+ testMethod(@Body(123 as unknown as string) _body: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: 123, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Body/Body.negative.unit.test.ts b/test/unit/controller/decorators/params/Body/Body.negative.unit.test.ts
new file mode 100644
index 0000000..b7df53f
--- /dev/null
+++ b/test/unit/controller/decorators/params/Body/Body.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Body: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_body: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Body/Body.positive.unit.test.ts b/test/unit/controller/decorators/params/Body/Body.positive.unit.test.ts
new file mode 100644
index 0000000..a1256b3
--- /dev/null
+++ b/test/unit/controller/decorators/params/Body/Body.positive.unit.test.ts
@@ -0,0 +1,48 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Body, RequestBody } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Body / @RequestBody: Positive", () => {
+ it("should define correct metadata for a @Body with a key", () => {
+ class TestController {
+ testMethod(@Body("data") _data: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: "data", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @Body without a key (full body injection)", () => {
+ class TestController {
+ testMethod(@Body() _fullBody: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: undefined, index: 0 }
+ });
+ });
+
+ it("RequestBody should be an alias for Body", () => {
+ class TestController {
+ testMethod(@RequestBody("user") _user: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: "user", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Event/Event.boundary.unit.test.ts b/test/unit/controller/decorators/params/Event/Event.boundary.unit.test.ts
new file mode 100644
index 0000000..6aba5d6
--- /dev/null
+++ b/test/unit/controller/decorators/params/Event/Event.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Event } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Event: Boundary", () => {
+ it("should store null as key if provided", () => {
+ class TestController {
+ testMethod(@Event(null as unknown as string) _event: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "EVENT:0": { type: ParamSource.EVENT, key: null, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Event/Event.negative.unit.test.ts b/test/unit/controller/decorators/params/Event/Event.negative.unit.test.ts
new file mode 100644
index 0000000..408cf25
--- /dev/null
+++ b/test/unit/controller/decorators/params/Event/Event.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Event: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_event: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Event/Event.positive.unit.test.ts b/test/unit/controller/decorators/params/Event/Event.positive.unit.test.ts
new file mode 100644
index 0000000..4d147d5
--- /dev/null
+++ b/test/unit/controller/decorators/params/Event/Event.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Event } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Event: Positive", () => {
+ it("should define correct metadata for @Event", () => {
+ class TestController {
+ testMethod(@Event() _event: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "EVENT:0": { type: ParamSource.EVENT, key: undefined, index: 0 }
+ });
+ });
+
+ it("should define key as passed for @Event if provided", () => {
+ class TestController {
+ testMethod(@Event("someKey" as unknown as string) _event: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "EVENT:0": { type: ParamSource.EVENT, key: "someKey", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Headers/Headers.boundary.unit.test.ts b/test/unit/controller/decorators/params/Headers/Headers.boundary.unit.test.ts
new file mode 100644
index 0000000..ce8d1f1
--- /dev/null
+++ b/test/unit/controller/decorators/params/Headers/Headers.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Headers } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Headers: Boundary", () => {
+ it("should store null as key if provided", () => {
+ class TestController {
+ testMethod(@Headers(null as unknown as string) _headers: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "HEADERS:0": { type: ParamSource.HEADERS, key: null, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Headers/Headers.negative.unit.test.ts b/test/unit/controller/decorators/params/Headers/Headers.negative.unit.test.ts
new file mode 100644
index 0000000..16ef92f
--- /dev/null
+++ b/test/unit/controller/decorators/params/Headers/Headers.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Headers: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_headers: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Headers/Headers.positive.unit.test.ts b/test/unit/controller/decorators/params/Headers/Headers.positive.unit.test.ts
new file mode 100644
index 0000000..9e00e04
--- /dev/null
+++ b/test/unit/controller/decorators/params/Headers/Headers.positive.unit.test.ts
@@ -0,0 +1,41 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Headers } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Headers: Positive", () => {
+ it("should define correct metadata for @Headers with a key", () => {
+ class TestController {
+ testMethod(@Headers("Authorization") _auth: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "HEADERS:0": {
+ type: ParamSource.HEADERS,
+ key: "Authorization",
+ index: 0
+ }
+ });
+ });
+
+ it("should define correct metadata for @Headers without a key (full headers object)", () => {
+ class TestController {
+ testMethod(@Headers() _headers: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "HEADERS:0": { type: ParamSource.HEADERS, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts b/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts
new file mode 100644
index 0000000..3e8f66c
--- /dev/null
+++ b/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts
@@ -0,0 +1,65 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import {
+ Body,
+ Event,
+ Headers,
+ Param,
+ Query,
+ Request,
+ Response
+} from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("HTTP Parameter Decorators: Integration", () => {
+ it("should define correct metadata for multiple parameters in the same method", () => {
+ class TestController {
+ testMethod(
+ @Param("userId") _id: string,
+ @Query("search") _search: string,
+ @Body() _body: unknown,
+ @Event() _event: unknown,
+ @Headers("Accept") _accept: string,
+ @Request() _req: unknown,
+ @Response("ok") _ok: boolean
+ ) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "userId", index: 0 },
+ "QUERY:1": { type: ParamSource.QUERY, key: "search", index: 1 },
+ "BODY:2": { type: ParamSource.BODY, key: undefined, index: 2 },
+ "EVENT:3": { type: ParamSource.EVENT, key: undefined, index: 3 },
+ "HEADERS:4": { type: ParamSource.HEADERS, key: "Accept", index: 4 },
+ "REQUEST:5": { type: ParamSource.REQUEST, key: undefined, index: 5 },
+ "RESPONSE:6": { type: ParamSource.RESPONSE, key: "ok", index: 6 }
+ });
+ });
+
+ it("should define correct metadata for parameters in different methods", () => {
+ class TestController {
+ methodOne(@Param("id") _id: string) {}
+ methodTwo(@Query("page") _page: number) {}
+ }
+
+ const metadataOne = getParameterMetadata(TestController, "methodOne");
+ expect(metadataOne).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
+ });
+
+ const metadataTwo = getParameterMetadata(TestController, "methodTwo");
+ expect(metadataTwo).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "page", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Param/Param.boundary.unit.test.ts b/test/unit/controller/decorators/params/Param/Param.boundary.unit.test.ts
new file mode 100644
index 0000000..e7a5013
--- /dev/null
+++ b/test/unit/controller/decorators/params/Param/Param.boundary.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Param } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Param: Boundary", () => {
+ it("should store null as key if provided", () => {
+ class TestController {
+ testMethod(@Param(null as unknown as string) __param: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: null, index: 0 }
+ });
+ });
+
+ it("should store empty string as key if provided", () => {
+ class TestController {
+ testMethod(@Param("") __param: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Param/Param.negative.unit.test.ts b/test/unit/controller/decorators/params/Param/Param.negative.unit.test.ts
new file mode 100644
index 0000000..d328bfc
--- /dev/null
+++ b/test/unit/controller/decorators/params/Param/Param.negative.unit.test.ts
@@ -0,0 +1,31 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Param: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied to a method", () => {
+ class TestController {
+ someMethod(_arg1: string, _arg2: number) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "someMethod");
+ expect(metadata).toEqual({});
+ });
+
+ it("should not define any parameter metadata for a method that does not exist", () => {
+ class TestController {
+ existingMethod() {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "nonExistentMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Param/Param.positive.unit.test.ts b/test/unit/controller/decorators/params/Param/Param.positive.unit.test.ts
new file mode 100644
index 0000000..b626b98
--- /dev/null
+++ b/test/unit/controller/decorators/params/Param/Param.positive.unit.test.ts
@@ -0,0 +1,48 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Param, PathVariable } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Param / @PathVariable: Positive", () => {
+ it("should define correct metadata for a @Param with a key", () => {
+ class TestController {
+ testMethod(@Param("id") _id: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @Param without a key (full object injection)", () => {
+ class TestController {
+ testMethod(@Param() __params: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: undefined, index: 0 }
+ });
+ });
+
+ it("PathVariable should be an alias for Param", () => {
+ class TestController {
+ testMethod(@PathVariable("name") _name: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "name", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/PathVariable/PathVariable.boundary.unit.test.ts b/test/unit/controller/decorators/params/PathVariable/PathVariable.boundary.unit.test.ts
new file mode 100644
index 0000000..4fd4e3a
--- /dev/null
+++ b/test/unit/controller/decorators/params/PathVariable/PathVariable.boundary.unit.test.ts
@@ -0,0 +1,39 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { PathVariable } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@PathVariable: Boundary", () => {
+ it("should handle multiple @PathVariable decorators on the same parameter", () => {
+ class TestController {
+ testMethod(@PathVariable("id") @PathVariable("alias") _id: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ // Usually the one closest to the parameter wins, or they overwrite each other.
+ // In our implementation (assignParamMetadata), it uses the same key for the map "TYPE:INDEX".
+ // So the last one (outermost in decorator execution order) wins?
+ // Wait, decorators are executed bottom-up.
+ // @D1
+ // @D2
+ // method(@D3 param)
+ // D3 runs first.
+
+ // In our case: @PathVariable("id") @PathVariable("alias") id
+ // "alias" is D2, "id" is D1. D2 runs first, then D1.
+ // So "id" should win.
+
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/PathVariable/PathVariable.negative.unit.test.ts b/test/unit/controller/decorators/params/PathVariable/PathVariable.negative.unit.test.ts
new file mode 100644
index 0000000..a32774b
--- /dev/null
+++ b/test/unit/controller/decorators/params/PathVariable/PathVariable.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@PathVariable: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ someMethod(_arg1: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "someMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/PathVariable/PathVariable.positive.unit.test.ts b/test/unit/controller/decorators/params/PathVariable/PathVariable.positive.unit.test.ts
new file mode 100644
index 0000000..f682900
--- /dev/null
+++ b/test/unit/controller/decorators/params/PathVariable/PathVariable.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { PathVariable } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@PathVariable: Positive", () => {
+ it("should define correct metadata for a @PathVariable with a key", () => {
+ class TestController {
+ testMethod(@PathVariable("id") _id: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: "id", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @PathVariable without a key", () => {
+ class TestController {
+ testMethod(@PathVariable() __params: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "PARAM:0": { type: ParamSource.PARAM, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Query/Query.boundary.unit.test.ts b/test/unit/controller/decorators/params/Query/Query.boundary.unit.test.ts
new file mode 100644
index 0000000..bc8d148
--- /dev/null
+++ b/test/unit/controller/decorators/params/Query/Query.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Query } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Query: Boundary", () => {
+ it("should store empty string as key if provided", () => {
+ class TestController {
+ testMethod(@Query("") _query: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Query/Query.negative.unit.test.ts b/test/unit/controller/decorators/params/Query/Query.negative.unit.test.ts
new file mode 100644
index 0000000..0a9862a
--- /dev/null
+++ b/test/unit/controller/decorators/params/Query/Query.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Query: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_query: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Query/Query.positive.unit.test.ts b/test/unit/controller/decorators/params/Query/Query.positive.unit.test.ts
new file mode 100644
index 0000000..e4fa75a
--- /dev/null
+++ b/test/unit/controller/decorators/params/Query/Query.positive.unit.test.ts
@@ -0,0 +1,48 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Query, RequestParam } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Query / @RequestParam: Positive", () => {
+ it("should define correct metadata for a @Query with a key", () => {
+ class TestController {
+ testMethod(@Query("search") _search: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "search", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @Query without a key", () => {
+ class TestController {
+ testMethod(@Query() _queryParams: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: undefined, index: 0 }
+ });
+ });
+
+ it("RequestParam should be an alias for Query", () => {
+ class TestController {
+ testMethod(@RequestParam("sort") _sort: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "sort", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Request/Request.boundary.unit.test.ts b/test/unit/controller/decorators/params/Request/Request.boundary.unit.test.ts
new file mode 100644
index 0000000..d153a0f
--- /dev/null
+++ b/test/unit/controller/decorators/params/Request/Request.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Request } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Request: Boundary", () => {
+ it("should store null as key if provided", () => {
+ class TestController {
+ testMethod(@Request(null as unknown as string) _req: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "REQUEST:0": { type: ParamSource.REQUEST, key: null, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Request/Request.negative.unit.test.ts b/test/unit/controller/decorators/params/Request/Request.negative.unit.test.ts
new file mode 100644
index 0000000..d49c6ea
--- /dev/null
+++ b/test/unit/controller/decorators/params/Request/Request.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Request: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_req: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Request/Request.positive.unit.test.ts b/test/unit/controller/decorators/params/Request/Request.positive.unit.test.ts
new file mode 100644
index 0000000..ce997b9
--- /dev/null
+++ b/test/unit/controller/decorators/params/Request/Request.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Request } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Request: Positive", () => {
+ it("should define correct metadata for @Request with a key", () => {
+ class TestController {
+ testMethod(@Request("method") _method: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "REQUEST:0": { type: ParamSource.REQUEST, key: "method", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for @Request without a key (full request object)", () => {
+ class TestController {
+ testMethod(@Request() _req: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "REQUEST:0": { type: ParamSource.REQUEST, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestBody/RequestBody.boundary.unit.test.ts b/test/unit/controller/decorators/params/RequestBody/RequestBody.boundary.unit.test.ts
new file mode 100644
index 0000000..81455d3
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestBody/RequestBody.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { RequestBody } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestBody: Boundary", () => {
+ it("should handle multiple @RequestBody decorators on the same parameter", () => {
+ class TestController {
+ testMethod(@RequestBody("data") @RequestBody("raw") _body: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: "data", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestBody/RequestBody.negative.unit.test.ts b/test/unit/controller/decorators/params/RequestBody/RequestBody.negative.unit.test.ts
new file mode 100644
index 0000000..fbad3a6
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestBody/RequestBody.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestBody: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ someMethod(_arg1: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "someMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestBody/RequestBody.positive.unit.test.ts b/test/unit/controller/decorators/params/RequestBody/RequestBody.positive.unit.test.ts
new file mode 100644
index 0000000..b80d3d5
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestBody/RequestBody.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { RequestBody } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestBody: Positive", () => {
+ it("should define correct metadata for a @RequestBody with a key", () => {
+ class TestController {
+ testMethod(@RequestBody("data") _data: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: "data", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @RequestBody without a key", () => {
+ class TestController {
+ testMethod(@RequestBody() _body: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "BODY:0": { type: ParamSource.BODY, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestParam/RequestParam.boundary.unit.test.ts b/test/unit/controller/decorators/params/RequestParam/RequestParam.boundary.unit.test.ts
new file mode 100644
index 0000000..79d68dc
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestParam/RequestParam.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { RequestParam } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestParam: Boundary", () => {
+ it("should handle multiple @RequestParam decorators on the same parameter", () => {
+ class TestController {
+ testMethod(@RequestParam("name") @RequestParam("alias") _name: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "name", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestParam/RequestParam.negative.unit.test.ts b/test/unit/controller/decorators/params/RequestParam/RequestParam.negative.unit.test.ts
new file mode 100644
index 0000000..fc0dcd4
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestParam/RequestParam.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestParam: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ someMethod(_arg1: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "someMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/RequestParam/RequestParam.positive.unit.test.ts b/test/unit/controller/decorators/params/RequestParam/RequestParam.positive.unit.test.ts
new file mode 100644
index 0000000..794bd37
--- /dev/null
+++ b/test/unit/controller/decorators/params/RequestParam/RequestParam.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { RequestParam } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@RequestParam: Positive", () => {
+ it("should define correct metadata for a @RequestParam with a key", () => {
+ class TestController {
+ testMethod(@RequestParam("name") _name: string) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "name", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for a @RequestParam without a key", () => {
+ class TestController {
+ testMethod(@RequestParam() _query: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Response/Response.boundary.unit.test.ts b/test/unit/controller/decorators/params/Response/Response.boundary.unit.test.ts
new file mode 100644
index 0000000..612050e
--- /dev/null
+++ b/test/unit/controller/decorators/params/Response/Response.boundary.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Response } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Response: Boundary", () => {
+ it("should store null as key if provided", () => {
+ class TestController {
+ testMethod(@Response(null as unknown as string) _res: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "RESPONSE:0": { type: ParamSource.RESPONSE, key: null, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/params/Response/Response.negative.unit.test.ts b/test/unit/controller/decorators/params/Response/Response.negative.unit.test.ts
new file mode 100644
index 0000000..3ecd2d2
--- /dev/null
+++ b/test/unit/controller/decorators/params/Response/Response.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Response: Negative", () => {
+ it("should not define any parameter metadata if no decorator is applied", () => {
+ class TestController {
+ testMethod(_res: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({});
+ });
+});
diff --git a/test/unit/controller/decorators/params/Response/Response.positive.unit.test.ts b/test/unit/controller/decorators/params/Response/Response.positive.unit.test.ts
new file mode 100644
index 0000000..4b659d3
--- /dev/null
+++ b/test/unit/controller/decorators/params/Response/Response.positive.unit.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { Response } from "src/controller/decorators/params";
+import { ParamSource } from "src/domain/enums";
+
+function getParameterMetadata(
+ target: object,
+ propertyKey: string | symbol
+): Record {
+ const metadataTarget = (target as { prototype: object }).prototype;
+ return Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, metadataTarget, propertyKey) || {};
+}
+
+describe("@Response: Positive", () => {
+ it("should define correct metadata for @Response with a key", () => {
+ class TestController {
+ testMethod(@Response("status") _status: number) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "RESPONSE:0": { type: ParamSource.RESPONSE, key: "status", index: 0 }
+ });
+ });
+
+ it("should define correct metadata for @Response without a key (full response object)", () => {
+ class TestController {
+ testMethod(@Response() _res: unknown) {}
+ }
+
+ const metadata = getParameterMetadata(TestController, "testMethod");
+ expect(metadata).toEqual({
+ "RESPONSE:0": { type: ParamSource.RESPONSE, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Delete/Delete.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Delete/Delete.boundary.unit.test.ts
new file mode 100644
index 0000000..752ee91
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Delete/Delete.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Delete } from "src/controller/decorators/routing";
+
+describe("@Delete: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Delete(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Delete("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Delete/Delete.negative.unit.test.ts b/test/unit/controller/decorators/routing/Delete/Delete.negative.unit.test.ts
new file mode 100644
index 0000000..3765f5a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Delete/Delete.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Delete: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Delete/Delete.positive.unit.test.ts b/test/unit/controller/decorators/routing/Delete/Delete.positive.unit.test.ts
new file mode 100644
index 0000000..e31b334
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Delete/Delete.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Delete, DeleteMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Delete / @DeleteMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Delete(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Delete()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Delete(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Delete(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("DeleteMapping should be an alias for Delete", () => {
+ class TestClass {
+ @DeleteMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..11b77a0
--- /dev/null
+++ b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { DeleteMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@DeleteMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @DeleteMapping("/first")
+ @DeleteMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.negative.unit.test.ts
new file mode 100644
index 0000000..f54efb8
--- /dev/null
+++ b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@DeleteMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.positive.unit.test.ts
new file mode 100644
index 0000000..37a6e59
--- /dev/null
+++ b/test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { DeleteMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@DeleteMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @DeleteMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @DeleteMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.DELETE);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Get/Get.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Get/Get.boundary.unit.test.ts
new file mode 100644
index 0000000..1ae852a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Get/Get.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Get } from "src/controller/decorators/routing";
+
+describe("@Get: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Get(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Get("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Get/Get.negative.unit.test.ts b/test/unit/controller/decorators/routing/Get/Get.negative.unit.test.ts
new file mode 100644
index 0000000..29ae800
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Get/Get.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Get: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Get/Get.positive.unit.test.ts b/test/unit/controller/decorators/routing/Get/Get.positive.unit.test.ts
new file mode 100644
index 0000000..ec094cf
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Get/Get.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Get, GetMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Get / @GetMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Get(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Get()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Get(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Get(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("GetMapping should be an alias for Get", () => {
+ class TestClass {
+ @GetMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/GetMapping/GetMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/GetMapping/GetMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..9579e4a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/GetMapping/GetMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { GetMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@GetMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @GetMapping("/first")
+ @GetMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/GetMapping/GetMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/GetMapping/GetMapping.negative.unit.test.ts
new file mode 100644
index 0000000..9772c91
--- /dev/null
+++ b/test/unit/controller/decorators/routing/GetMapping/GetMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@GetMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/GetMapping/GetMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/GetMapping/GetMapping.positive.unit.test.ts
new file mode 100644
index 0000000..9d2205e
--- /dev/null
+++ b/test/unit/controller/decorators/routing/GetMapping/GetMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { GetMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@GetMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @GetMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @GetMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.GET);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Head/Head.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Head/Head.boundary.unit.test.ts
new file mode 100644
index 0000000..e7b3173
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Head/Head.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Head } from "src/controller/decorators/routing";
+
+describe("@Head: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Head(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Head("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Head/Head.negative.unit.test.ts b/test/unit/controller/decorators/routing/Head/Head.negative.unit.test.ts
new file mode 100644
index 0000000..fe05049
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Head/Head.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Head: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Head/Head.positive.unit.test.ts b/test/unit/controller/decorators/routing/Head/Head.positive.unit.test.ts
new file mode 100644
index 0000000..298be5b
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Head/Head.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Head, HeadMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Head / @HeadMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Head(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Head()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Head(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Head(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("HeadMapping should be an alias for Head", () => {
+ class TestClass {
+ @HeadMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..ca78925
--- /dev/null
+++ b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { HeadMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@HeadMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @HeadMapping("/first")
+ @HeadMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.negative.unit.test.ts
new file mode 100644
index 0000000..e11f18a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@HeadMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.positive.unit.test.ts
new file mode 100644
index 0000000..0f1000a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/HeadMapping/HeadMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { HeadMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@HeadMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @HeadMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @HeadMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.HEAD);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Integration/Integration.positive.unit.test.ts b/test/unit/controller/decorators/routing/Integration/Integration.positive.unit.test.ts
new file mode 100644
index 0000000..73bb58c
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Integration/Integration.positive.unit.test.ts
@@ -0,0 +1,30 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Get, Post } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("HTTP Method Decorators: Integration", () => {
+ it("should not interfere with metadata of other methods in the same class", () => {
+ class TestClass {
+ @Post("/method1")
+ method1() {}
+
+ @Get("/method2")
+ method2() {}
+ }
+
+ const method1Function = TestClass.prototype.method1;
+ const method2Function = TestClass.prototype.method2;
+
+ const method1Path = Reflect.getMetadata(PATH_METADATA, method1Function);
+ const method1Method = Reflect.getMetadata(METHOD_METADATA, method1Function);
+ expect(method1Path).toBe("/method1");
+ expect(method1Method).toBe(RequestMethod.POST);
+
+ const method2Path = Reflect.getMetadata(PATH_METADATA, method2Function);
+ const method2Method = Reflect.getMetadata(METHOD_METADATA, method2Function);
+ expect(method2Path).toBe("/method2");
+ expect(method2Method).toBe(RequestMethod.GET);
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Options/Options.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Options/Options.boundary.unit.test.ts
new file mode 100644
index 0000000..20f4100
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Options/Options.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Options } from "src/controller/decorators/routing";
+
+describe("@Options: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Options(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Options("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Options/Options.negative.unit.test.ts b/test/unit/controller/decorators/routing/Options/Options.negative.unit.test.ts
new file mode 100644
index 0000000..55296b4
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Options/Options.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Options: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Options/Options.positive.unit.test.ts b/test/unit/controller/decorators/routing/Options/Options.positive.unit.test.ts
new file mode 100644
index 0000000..2eba20c
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Options/Options.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Options, OptionsMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Options / @OptionsMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Options(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Options()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Options(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Options(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("OptionsMapping should be an alias for Options", () => {
+ class TestClass {
+ @OptionsMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..f48e991
--- /dev/null
+++ b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { OptionsMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@OptionsMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @OptionsMapping("/first")
+ @OptionsMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.negative.unit.test.ts
new file mode 100644
index 0000000..54cb568
--- /dev/null
+++ b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@OptionsMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.positive.unit.test.ts
new file mode 100644
index 0000000..24b7300
--- /dev/null
+++ b/test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { OptionsMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@OptionsMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @OptionsMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @OptionsMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.OPTIONS);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Patch/Patch.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Patch/Patch.boundary.unit.test.ts
new file mode 100644
index 0000000..b603c73
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Patch/Patch.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Patch } from "src/controller/decorators/routing";
+
+describe("@Patch: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Patch(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Patch("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Patch/Patch.negative.unit.test.ts b/test/unit/controller/decorators/routing/Patch/Patch.negative.unit.test.ts
new file mode 100644
index 0000000..54b6552
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Patch/Patch.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Patch: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Patch/Patch.positive.unit.test.ts b/test/unit/controller/decorators/routing/Patch/Patch.positive.unit.test.ts
new file mode 100644
index 0000000..6eca58b
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Patch/Patch.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Patch, PatchMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Patch / @PatchMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Patch(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Patch()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Patch(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Patch(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("PatchMapping should be an alias for Patch", () => {
+ class TestClass {
+ @PatchMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..8561f8b
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PatchMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PatchMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @PatchMapping("/first")
+ @PatchMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.negative.unit.test.ts
new file mode 100644
index 0000000..74ce7de
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@PatchMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.positive.unit.test.ts
new file mode 100644
index 0000000..7639811
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PatchMapping/PatchMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PatchMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PatchMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @PatchMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @PatchMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PATCH);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Post/Post.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Post/Post.boundary.unit.test.ts
new file mode 100644
index 0000000..10fab2e
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Post/Post.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Post } from "src/controller/decorators/routing";
+
+describe("@Post: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Post(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Post("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Post/Post.negative.unit.test.ts b/test/unit/controller/decorators/routing/Post/Post.negative.unit.test.ts
new file mode 100644
index 0000000..c9fa8b0
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Post/Post.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Post: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Post/Post.positive.unit.test.ts b/test/unit/controller/decorators/routing/Post/Post.positive.unit.test.ts
new file mode 100644
index 0000000..35f4612
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Post/Post.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Post, PostMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Post / @PostMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Post(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Post()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Post(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Post(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("PostMapping should be an alias for Post", () => {
+ class TestClass {
+ @PostMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PostMapping/PostMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/PostMapping/PostMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..367521f
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PostMapping/PostMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PostMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PostMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @PostMapping("/first")
+ @PostMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PostMapping/PostMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/PostMapping/PostMapping.negative.unit.test.ts
new file mode 100644
index 0000000..2c7ba13
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PostMapping/PostMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@PostMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PostMapping/PostMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/PostMapping/PostMapping.positive.unit.test.ts
new file mode 100644
index 0000000..6142ecd
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PostMapping/PostMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PostMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PostMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @PostMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @PostMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.POST);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Put/Put.boundary.unit.test.ts b/test/unit/controller/decorators/routing/Put/Put.boundary.unit.test.ts
new file mode 100644
index 0000000..208dbf3
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Put/Put.boundary.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { PATH_METADATA } from "src/domain/constants";
+import { Put } from "src/controller/decorators/routing";
+
+describe("@Put: Boundary", () => {
+ it('should handle undefined path by defaulting to "/"', () => {
+ class TestClass {
+ @Put(undefined)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it('should correctly handle an empty string as path by defaulting to "/"', () => {
+ class TestClass {
+ @Put("")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Put/Put.negative.unit.test.ts b/test/unit/controller/decorators/routing/Put/Put.negative.unit.test.ts
new file mode 100644
index 0000000..e4c9bf5
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Put/Put.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@Put: Negative", () => {
+ it("should not define metadata if the decorator is not applied", () => {
+ class TestClass {
+ anotherMethod() {}
+ }
+ const methodFunction = TestClass.prototype.anotherMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/Put/Put.positive.unit.test.ts b/test/unit/controller/decorators/routing/Put/Put.positive.unit.test.ts
new file mode 100644
index 0000000..5db811b
--- /dev/null
+++ b/test/unit/controller/decorators/routing/Put/Put.positive.unit.test.ts
@@ -0,0 +1,58 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { Put, PutMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@Put / @PutMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @Put(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @Put()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+
+ it("should correctly handle a path with a trailing slash", () => {
+ const trailingSlashPath = "/users/";
+ class TestClass {
+ @Put(trailingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(trailingSlashPath);
+ });
+
+ it("should correctly handle a path without a leading slash", () => {
+ const noLeadingSlashPath = "users/profile";
+ class TestClass {
+ @Put(noLeadingSlashPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(noLeadingSlashPath);
+ });
+
+ it("PutMapping should be an alias for Put", () => {
+ class TestClass {
+ @PutMapping("/alias")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/alias");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PutMapping/PutMapping.boundary.unit.test.ts b/test/unit/controller/decorators/routing/PutMapping/PutMapping.boundary.unit.test.ts
new file mode 100644
index 0000000..7ea4b9b
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PutMapping/PutMapping.boundary.unit.test.ts
@@ -0,0 +1,18 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PutMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PutMapping: Boundary", () => {
+ it("should handle multiple decorators (last one wins)", () => {
+ class TestClass {
+ @PutMapping("/first")
+ @PutMapping("/second")
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/first");
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PutMapping/PutMapping.negative.unit.test.ts b/test/unit/controller/decorators/routing/PutMapping/PutMapping.negative.unit.test.ts
new file mode 100644
index 0000000..14b9c17
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PutMapping/PutMapping.negative.unit.test.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+
+describe("@PutMapping: Negative", () => {
+ it("should not define metadata if decorator is not applied", () => {
+ class TestClass {
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBeUndefined();
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBeUndefined();
+ });
+});
diff --git a/test/unit/controller/decorators/routing/PutMapping/PutMapping.positive.unit.test.ts b/test/unit/controller/decorators/routing/PutMapping/PutMapping.positive.unit.test.ts
new file mode 100644
index 0000000..360477a
--- /dev/null
+++ b/test/unit/controller/decorators/routing/PutMapping/PutMapping.positive.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { PutMapping } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+
+describe("@PutMapping: Positive", () => {
+ it("should define METHOD_METADATA and PATH_METADATA with a given path", () => {
+ const testPath = "/custom-path";
+ class TestClass {
+ @PutMapping(testPath)
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe(testPath);
+ });
+
+ it('should use "/" as the default path if none is provided', () => {
+ class TestClass {
+ @PutMapping()
+ testMethod() {}
+ }
+ const methodFunction = TestClass.prototype.testMethod;
+ expect(Reflect.getMetadata(METHOD_METADATA, methodFunction)).toBe(RequestMethod.PUT);
+ expect(Reflect.getMetadata(PATH_METADATA, methodFunction)).toBe("/");
+ });
+});
diff --git a/test/unit/exceptions/AppException/AppException.positive.unit.test.ts b/test/unit/exceptions/AppException/AppException.positive.unit.test.ts
new file mode 100644
index 0000000..b4bc2cc
--- /dev/null
+++ b/test/unit/exceptions/AppException/AppException.positive.unit.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import { AppException } from "src/exceptions";
+
+describe("AppException: Positive", () => {
+ it("should create an exception with message and status", () => {
+ const msg = "Error occurred";
+ const status = 400;
+ const ex = new AppException(msg, status);
+
+ expect(ex.message).toBe(msg);
+ expect(ex.status).toBe(status);
+ expect(ex.name).toBe("AppException");
+ });
+
+ it("should use default status 500", () => {
+ const ex = new AppException("Error");
+ expect(ex.status).toBe(500);
+ });
+});
diff --git a/test/unit/exceptions/HttpException/HttpException.positive.unit.test.ts b/test/unit/exceptions/HttpException/HttpException.positive.unit.test.ts
new file mode 100644
index 0000000..cae8056
--- /dev/null
+++ b/test/unit/exceptions/HttpException/HttpException.positive.unit.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, it } from "vitest";
+import { HttpException } from "src/exceptions";
+
+describe("HttpException: Positive", () => {
+ it("should create an exception with message and status", () => {
+ const msg = "Not Found";
+ const status = 404;
+ const ex = new HttpException(msg, status);
+
+ expect(ex.message).toBe(msg);
+ expect(ex.status).toBe(status);
+ expect(ex.name).toBe("HttpException");
+ });
+});
diff --git a/test/unit/repository/MetadataRepository/MetadataRepository.extra.unit.test.ts b/test/unit/repository/MetadataRepository/MetadataRepository.extra.unit.test.ts
new file mode 100644
index 0000000..92bd45b
--- /dev/null
+++ b/test/unit/repository/MetadataRepository/MetadataRepository.extra.unit.test.ts
@@ -0,0 +1,29 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it } from "vitest";
+import { MetadataRepository } from "src/repository";
+
+describe("MetadataRepository: Extra", () => {
+ let repository: MetadataRepository;
+
+ beforeEach(() => {
+ repository = new MetadataRepository();
+ });
+
+ it("should define and get metadata for a property", () => {
+ const target = { prop: "value" };
+ repository.defineMetadata("key", "meta-value", target, "prop");
+
+ expect(repository.getMetadata("key", target, "prop")).toBe("meta-value");
+ expect(repository.hasMetadata("key", target, "prop")).toBe(true);
+ });
+
+ it("should get own metadata keys for a property", () => {
+ const target = { prop: "value" };
+ repository.defineMetadata("key1", "val1", target, "prop");
+ repository.defineMetadata("key2", "val2", target, "prop");
+
+ const keys = repository.getOwnMetadataKeys(target, "prop");
+ expect(keys).toContain("key1");
+ expect(keys).toContain("key2");
+ });
+});
diff --git a/test/unit/repository/MetadataRepository/MetadataRepository.positive.unit.test.ts b/test/unit/repository/MetadataRepository/MetadataRepository.positive.unit.test.ts
new file mode 100644
index 0000000..a9543de
--- /dev/null
+++ b/test/unit/repository/MetadataRepository/MetadataRepository.positive.unit.test.ts
@@ -0,0 +1,23 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { MetadataRepository } from "src/repository";
+
+describe("MetadataRepository: Positive", () => {
+ const repo = new MetadataRepository();
+
+ it("should define and get metadata", () => {
+ const target = {};
+ const key = "key";
+ const val = "value";
+ repo.defineMetadata(key, val, target);
+ expect(repo.getMetadata(key, target)).toBe(val);
+ });
+
+ it("should check if metadata exists", () => {
+ const target = {};
+ const key = "key";
+ repo.defineMetadata(key, "val", target);
+ expect(repo.hasMetadata(key, target)).toBe(true);
+ expect(repo.hasMetadata("other", target)).toBe(false);
+ });
+});
diff --git a/test/unit/repository/assignInjectMetadata/assignInjectMetadata.positive.unit.test.ts b/test/unit/repository/assignInjectMetadata/assignInjectMetadata.positive.unit.test.ts
new file mode 100644
index 0000000..fe67231
--- /dev/null
+++ b/test/unit/repository/assignInjectMetadata/assignInjectMetadata.positive.unit.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, it } from "vitest";
+import { assignInjectMetadata } from "src/repository";
+import { ParamSource } from "src/domain/enums";
+
+describe("assignInjectMetadata: Positive", () => {
+ it("should correctly update existing inject metadata object", () => {
+ const existing = {};
+ const updated = assignInjectMetadata(existing, 0, "TOKEN");
+
+ expect(updated).toEqual({
+ "INJECT:0": { type: ParamSource.INJECT, token: "TOKEN", index: 0 }
+ });
+ });
+});
diff --git a/test/unit/repository/assignParamMetadata/assignParamMetadata.positive.unit.test.ts b/test/unit/repository/assignParamMetadata/assignParamMetadata.positive.unit.test.ts
new file mode 100644
index 0000000..0529a62
--- /dev/null
+++ b/test/unit/repository/assignParamMetadata/assignParamMetadata.positive.unit.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vitest";
+import { assignParamMetadata } from "src/repository";
+import { ParamSource } from "src/domain/enums";
+
+describe("assignParamMetadata: Positive", () => {
+ it("should correctly update existing metadata object", () => {
+ const existing = {
+ "QUERY:0": { type: ParamSource.QUERY, key: "name", index: 0 }
+ };
+ const updated = assignParamMetadata(existing, 1, ParamSource.BODY, "data");
+
+ expect(updated).toEqual({
+ "QUERY:0": { type: ParamSource.QUERY, key: "name", index: 0 },
+ "BODY:1": { type: ParamSource.BODY, key: "data", index: 1 }
+ });
+ });
+});
diff --git a/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.boundary.unit.test.ts b/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.boundary.unit.test.ts
new file mode 100644
index 0000000..e59eaea
--- /dev/null
+++ b/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.boundary.unit.test.ts
@@ -0,0 +1,21 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createAppsScriptDecorator } from "src/repository";
+import { APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+
+describe("createAppsScriptDecorator: boundary", () => {
+ it("should use empty object as default options", () => {
+ const decorator = createAppsScriptDecorator(AppsScriptEventType.EDIT);
+ const methodDecorator = decorator(); // No options provided
+
+ class Test {
+ method() {}
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(Test.prototype, "method")!;
+ methodDecorator(Test.prototype, "method", descriptor);
+
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, Test.prototype.method)).toEqual({});
+ });
+});
diff --git a/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.positive.unit.test.ts b/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.positive.unit.test.ts
new file mode 100644
index 0000000..c36a647
--- /dev/null
+++ b/test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.positive.unit.test.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createAppsScriptDecorator } from "src/repository";
+import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "src/domain/constants";
+import { AppsScriptEventType } from "src/domain/enums";
+
+describe("createAppsScriptDecorator: positive", () => {
+ it("should define correct metadata on method", () => {
+ const eventType = AppsScriptEventType.OPEN;
+ const options = { foo: "bar" };
+ const decorator = createAppsScriptDecorator(eventType);
+ const methodDecorator = decorator(options);
+
+ class Test {
+ method() {}
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(Test.prototype, "method")!;
+ methodDecorator(Test.prototype, "method", descriptor);
+
+ expect(Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, Test.prototype.method)).toBe(eventType);
+ expect(Reflect.getMetadata(APPSSCRIPT_OPTIONS_METADATA, Test.prototype.method)).toEqual(
+ options
+ );
+ });
+});
diff --git a/test/unit/repository/createHttpDecorator/createHttpDecorator.boundary.unit.test.ts b/test/unit/repository/createHttpDecorator/createHttpDecorator.boundary.unit.test.ts
new file mode 100644
index 0000000..a02b995
--- /dev/null
+++ b/test/unit/repository/createHttpDecorator/createHttpDecorator.boundary.unit.test.ts
@@ -0,0 +1,35 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createHttpDecorator } from "src/repository";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { RequestMethod } from "src/domain/enums";
+
+describe("createHttpDecorator: boundary", () => {
+ it("should use '/' as default path if none is provided", () => {
+ const decorator = createHttpDecorator(RequestMethod.GET);
+ const methodDecorator = decorator();
+
+ class Test {
+ method() {}
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(Test.prototype, "method")!;
+ methodDecorator(Test.prototype, "method", descriptor);
+
+ expect(Reflect.getMetadata(PATH_METADATA, Test.prototype.method)).toBe("/");
+ });
+
+ it("should fallback to GET if method is falsy", () => {
+ const decorator = createHttpDecorator(null as unknown as RequestMethod);
+ const methodDecorator = decorator();
+
+ class Test {
+ method() {}
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(Test.prototype, "method")!;
+ methodDecorator(Test.prototype, "method", descriptor);
+
+ expect(Reflect.getMetadata(METHOD_METADATA, Test.prototype.method)).toBe(RequestMethod.GET);
+ });
+});
diff --git a/test/unit/repository/createHttpDecorator/createHttpDecorator.positive.unit.test.ts b/test/unit/repository/createHttpDecorator/createHttpDecorator.positive.unit.test.ts
new file mode 100644
index 0000000..401f0bd
--- /dev/null
+++ b/test/unit/repository/createHttpDecorator/createHttpDecorator.positive.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createHttpDecorator } from "src/repository";
+import { METHOD_METADATA, PATH_METADATA } from "src/domain/constants";
+import { RequestMethod } from "src/domain/enums";
+
+describe("createHttpDecorator: positive", () => {
+ it("should define correct method and path metadata", () => {
+ const method = RequestMethod.POST;
+ const path = "/users";
+ const decorator = createHttpDecorator(method);
+ const methodDecorator = decorator(path);
+
+ class Test {
+ method() {}
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(Test.prototype, "method")!;
+ methodDecorator(Test.prototype, "method", descriptor);
+
+ expect(Reflect.getMetadata(METHOD_METADATA, Test.prototype.method)).toBe(method);
+ expect(Reflect.getMetadata(PATH_METADATA, Test.prototype.method)).toBe(path);
+ });
+});
diff --git a/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts b/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts
new file mode 100644
index 0000000..ae39f8e
--- /dev/null
+++ b/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts
@@ -0,0 +1,36 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createParamDecorator } from "src/repository";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { ParamSource } from "src/domain/enums";
+
+describe("createParamDecorator: boundary", () => {
+ it("should handle multiple parameters on the same method", () => {
+ const queryDecorator = createParamDecorator(ParamSource.QUERY);
+ const bodyDecorator = createParamDecorator(ParamSource.BODY);
+
+ class Test {
+ method(@queryDecorator("id") _id: string, @bodyDecorator() _body: unknown) {}
+ }
+
+ const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method");
+ expect(metadata).toEqual({
+ [`${ParamSource.QUERY}:0`]: { type: ParamSource.QUERY, key: "id", index: 0 },
+ [`${ParamSource.BODY}:1`]: { type: ParamSource.BODY, key: undefined, index: 1 }
+ });
+ });
+
+ it("should handle parameters without a key", () => {
+ const decorator = createParamDecorator(ParamSource.HEADERS);
+ const headersDecorator = decorator();
+
+ class Test {
+ method(@headersDecorator _headers: unknown) {}
+ }
+
+ const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method");
+ expect(metadata).toEqual({
+ [`${ParamSource.HEADERS}:0`]: { type: ParamSource.HEADERS, key: undefined, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts b/test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts
new file mode 100644
index 0000000..825b952
--- /dev/null
+++ b/test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts
@@ -0,0 +1,39 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { createParamDecorator } from "src/repository";
+import { PARAM_DEFINITIONS_METADATA } from "src/domain/constants";
+import { ParamSource } from "src/domain/enums";
+
+describe("createParamDecorator: positive", () => {
+ it("should define parameter metadata for a method", () => {
+ const type = ParamSource.QUERY;
+ const key = "id";
+ const decorator = createParamDecorator(type);
+ const paramDecorator = decorator(key);
+
+ class Test {
+ method(@paramDecorator _param: string) {}
+ }
+
+ const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method");
+ expect(metadata).toEqual({
+ [`${type}:0`]: { type, key, index: 0 }
+ });
+ });
+
+ it("should define parameter metadata for a constructor", () => {
+ const type = ParamSource.INJECT;
+ const key = "service";
+ const decorator = createParamDecorator(type);
+ const paramDecorator = decorator(key);
+
+ class Test {
+ constructor(@paramDecorator __param: unknown) {}
+ }
+
+ const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test);
+ expect(metadata).toEqual({
+ [`${type}:0`]: { type, key, index: 0 }
+ });
+ });
+});
diff --git a/test/unit/repository/getInjectionTokens/getInjectionTokens.boundary.unit.test.ts b/test/unit/repository/getInjectionTokens/getInjectionTokens.boundary.unit.test.ts
new file mode 100644
index 0000000..112f769
--- /dev/null
+++ b/test/unit/repository/getInjectionTokens/getInjectionTokens.boundary.unit.test.ts
@@ -0,0 +1,19 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { getInjectionTokens } from "src/repository";
+
+describe("getInjectionTokens: boundary", () => {
+ it("should return an empty object if no metadata is present on class", () => {
+ class Test {}
+ const result = getInjectionTokens(Test);
+ expect(result).toEqual({});
+ });
+
+ it("should return an empty object if no metadata is present on method", () => {
+ class Test {
+ method() {}
+ }
+ const result = getInjectionTokens(Test.prototype, "method");
+ expect(result).toEqual({});
+ });
+});
diff --git a/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts b/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts
new file mode 100644
index 0000000..1b47741
--- /dev/null
+++ b/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts
@@ -0,0 +1,31 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { getInjectionTokens } from "src/repository";
+import { INJECT_TOKENS_METADATA } from "src/domain/constants";
+import { ParamSource } from "src/domain/enums";
+
+describe("getInjectionTokens: positive", () => {
+ it("should return injection tokens for a constructor", () => {
+ class Test {}
+ const tokens = {
+ [`${ParamSource.INJECT}:0`]: { type: ParamSource.INJECT, token: "ServiceA", index: 0 }
+ };
+ Reflect.defineMetadata(INJECT_TOKENS_METADATA, tokens, Test);
+
+ const result = getInjectionTokens(Test);
+ expect(result).toEqual(tokens);
+ });
+
+ it("should return injection tokens for a method", () => {
+ class Test {
+ method() {}
+ }
+ const tokens = {
+ [`${ParamSource.INJECT}:0`]: { type: ParamSource.INJECT, token: "ServiceB", index: 0 }
+ };
+ Reflect.defineMetadata(INJECT_TOKENS_METADATA, tokens, Test.prototype, "method");
+
+ const result = getInjectionTokens(Test.prototype, "method");
+ expect(result).toEqual(tokens);
+ });
+});
diff --git a/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts
new file mode 100644
index 0000000..bede5a0
--- /dev/null
+++ b/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it } from "vitest";
+import { BootApplication, BootApplicationFactory } from "src/controller";
+import { OnEdit } from "src/controller/decorators/appsscript";
+
+class TestEventController {
+ public onEditCalled = false;
+
+ @OnEdit({ range: "A1" })
+ handleEditA1() {
+ this.onEditCalled = true;
+ }
+}
+
+describe("EventDispatcher: Boundary", () => {
+ let controller: TestEventController;
+ let app: BootApplication;
+
+ beforeEach(() => {
+ controller = new TestEventController();
+ app = BootApplicationFactory.create({
+ controllers: [TestEventController],
+ providers: [{ provide: TestEventController, useValue: controller }]
+ });
+ });
+
+ it("should not throw error and not dispatch when event range is missing", async () => {
+ const mockEvent = {} as unknown as GoogleAppsScript.Events.SheetsOnEdit;
+ await expect(app.onEdit(mockEvent)).resolves.toBeUndefined();
+ expect(controller.onEditCalled).toBe(false);
+ });
+});
diff --git a/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts
new file mode 100644
index 0000000..c786987
--- /dev/null
+++ b/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts
@@ -0,0 +1,157 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { EventDispatcher, Resolver } from "src/service";
+import { AppsScriptEventType } from "src/domain/enums";
+import { Event } from "src/controller/decorators/params";
+import { Inject } from "src/controller/decorators";
+import { OnChange, OnEdit, OnFormSubmit } from "src/controller/decorators/appsscript";
+
+describe("EventDispatcher: Extra", () => {
+ let resolver: Resolver;
+ let dispatcher: EventDispatcher;
+
+ beforeEach(() => {
+ resolver = {
+ resolve: vi.fn()
+ } as unknown as Resolver;
+ });
+
+ it("should support RegExp in OnEdit filter", async () => {
+ class TestController {
+ public called = false;
+ @OnEdit({ range: /^A\d+$/ })
+ handle(@Event() _e: unknown) {
+ this.called = true;
+ }
+ }
+ const instance = new TestController();
+ vi.mocked(resolver.resolve).mockReturnValue(instance);
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: { getA1Notation: () => "A10" } });
+ expect(instance.called).toBe(true);
+
+ instance.called = false;
+ await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: { getA1Notation: () => "B1" } });
+ expect(instance.called).toBe(false);
+ });
+
+ it("should return false if range notation is missing in EDIT event", async () => {
+ class TestController {
+ public called = false;
+ @OnEdit({ range: "A1" })
+ handle() {
+ this.called = true;
+ }
+ }
+ const instance = new TestController();
+ vi.mocked(resolver.resolve).mockReturnValue(instance);
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: {} }); // Missing getA1Notation
+ expect(instance.called).toBe(false);
+ });
+
+ it("should filter OnFormSubmit by formId", async () => {
+ class TestController {
+ public called = false;
+ @OnFormSubmit({ formId: "FORM_1" })
+ handle() {
+ this.called = true;
+ }
+ }
+ const instance = new TestController();
+ vi.mocked(resolver.resolve).mockReturnValue(instance);
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, {
+ source: { getId: () => "FORM_1" }
+ });
+ expect(instance.called).toBe(true);
+
+ instance.called = false;
+ await dispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, {
+ source: { getId: () => "FORM_2" }
+ });
+ expect(instance.called).toBe(false);
+ });
+
+ it("should return false if formId is missing in FORM_SUBMIT event", async () => {
+ class TestController {
+ public called = false;
+ @OnFormSubmit({ formId: "FORM_1" })
+ handle() {
+ this.called = true;
+ }
+ }
+ const instance = new TestController();
+ vi.mocked(resolver.resolve).mockReturnValue(instance);
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, { source: {} }); // Missing getId
+ expect(instance.called).toBe(false);
+ });
+
+ it("should return false if changeType is missing in CHANGE event", async () => {
+ class TestController {
+ public called = false;
+ @OnChange({ changeType: "EDIT" })
+ handle() {
+ this.called = true;
+ }
+ }
+ const instance = new TestController();
+ vi.mocked(resolver.resolve).mockReturnValue(instance);
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.CHANGE, {}); // Missing changeType
+ expect(instance.called).toBe(false);
+ });
+
+ it("should support @Inject in event handlers", async () => {
+ class Service {}
+ class TestController {
+ public injectedService: unknown;
+ @OnEdit()
+ handle(@Inject(Service) s: Service) {
+ this.injectedService = s;
+ }
+ }
+ const serviceInstance = new Service();
+ const controllerInstance = new TestController();
+ vi.mocked(resolver.resolve).mockImplementation((target: unknown) => {
+ if (target === TestController) return controllerInstance;
+ if (target === Service) return serviceInstance;
+ return null;
+ });
+
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.EDIT, {
+ range: { getA1Notation: () => "A1" }
+ } as unknown as GoogleAppsScript.Events.SheetsOnEdit);
+ expect(controllerInstance.injectedService).toBe(serviceInstance);
+ });
+
+ it("should handle injection failure in event handlers", async () => {
+ class TestController {
+ public injectedValue: unknown = "initial";
+ @OnEdit()
+ handle(@Inject("UNKNOWN") s: unknown) {
+ this.injectedValue = s;
+ }
+ }
+ const controllerInstance = new TestController();
+ vi.mocked(resolver.resolve).mockImplementation((target: unknown) => {
+ if (target === TestController) return controllerInstance;
+ throw new Error("Resolve failed");
+ });
+
+ dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]]));
+
+ await dispatcher.dispatch(AppsScriptEventType.EDIT, {
+ range: { getA1Notation: () => "A1" }
+ } as unknown as GoogleAppsScript.Events.SheetsOnEdit);
+ expect(controllerInstance.injectedValue).toBeUndefined();
+ });
+});
diff --git a/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts
new file mode 100644
index 0000000..a63b59a
--- /dev/null
+++ b/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts
@@ -0,0 +1,50 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it } from "vitest";
+import { BootApplication, BootApplicationFactory } from "src/controller";
+import { OnChange, OnEdit } from "src/controller/decorators/appsscript";
+
+class TestEventController {
+ public onEditCalled = false;
+ public onChangeCalled = false;
+
+ @OnEdit({ range: "A1" })
+ handleEditA1() {
+ this.onEditCalled = true;
+ }
+
+ @OnChange({ changeType: "INSERT_ROW" })
+ handleChangeRow() {
+ this.onChangeCalled = true;
+ }
+}
+
+describe("EventDispatcher: Negative", () => {
+ let controller: TestEventController;
+ let app: BootApplication;
+
+ beforeEach(() => {
+ controller = new TestEventController();
+ app = BootApplicationFactory.create({
+ controllers: [TestEventController],
+ providers: [{ provide: TestEventController, useValue: controller }]
+ });
+ });
+
+ it("should not dispatch OnEdit event when range does not match", async () => {
+ const mockEvent = {
+ range: {
+ getA1Notation: () => "B2"
+ }
+ } as unknown as GoogleAppsScript.Events.SheetsOnEdit;
+ await app.onEdit(mockEvent);
+ expect(controller.onEditCalled).toBe(false);
+ });
+
+ it("should not dispatch OnChange event when changeType does not match", async () => {
+ const mockEvent = {
+ changeType: "REMOVE_ROW"
+ } as unknown as GoogleAppsScript.Events.SheetsOnChange;
+ await app.onChange(mockEvent);
+ expect(controller.onChangeCalled).toBe(false);
+ });
+});
diff --git a/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts
new file mode 100644
index 0000000..f598af8
--- /dev/null
+++ b/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts
@@ -0,0 +1,67 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it } from "vitest";
+import { BootApplication, BootApplicationFactory } from "src/controller";
+import { Event } from "src/controller/decorators/params";
+import { OnChange, OnEdit, OnOpen } from "src/controller/decorators/appsscript";
+
+class TestEventController {
+ public onOpenCalled = false;
+ public eventData: unknown = null;
+ public onEditCalled = false;
+ public onChangeCalled = false;
+
+ @OnOpen()
+ handleOpen(@Event() e: unknown) {
+ this.onOpenCalled = true;
+ this.eventData = e;
+ }
+
+ @OnEdit({ range: "A1" })
+ handleEditA1() {
+ this.onEditCalled = true;
+ }
+
+ @OnChange({ changeType: "INSERT_ROW" })
+ handleChangeRow() {
+ this.onChangeCalled = true;
+ }
+}
+
+describe("EventDispatcher: Positive", () => {
+ let controller: TestEventController;
+ let app: BootApplication;
+
+ beforeEach(() => {
+ controller = new TestEventController();
+ app = BootApplicationFactory.create({
+ controllers: [TestEventController],
+ providers: [{ provide: TestEventController, useValue: controller }]
+ });
+ });
+
+ it("should dispatch OnOpen event", async () => {
+ const mockEvent = { authMode: "LIMITED" } as unknown as GoogleAppsScript.Events.AppsScriptEvent;
+ await app.onOpen(mockEvent);
+
+ expect(controller.onOpenCalled).toBe(true);
+ expect(controller.eventData).toEqual(mockEvent);
+ });
+
+ it("should dispatch OnEdit event when range matches", async () => {
+ const mockEvent = {
+ range: {
+ getA1Notation: () => "A1"
+ }
+ } as unknown as GoogleAppsScript.Events.SheetsOnEdit;
+ await app.onEdit(mockEvent);
+ expect(controller.onEditCalled).toBe(true);
+ });
+
+ it("should dispatch OnChange event when changeType matches", async () => {
+ const mockEvent = {
+ changeType: "INSERT_ROW"
+ } as unknown as GoogleAppsScript.Events.SheetsOnChange;
+ await app.onChange(mockEvent);
+ expect(controller.onChangeCalled).toBe(true);
+ });
+});
diff --git a/test/unit/service/PathMatcher/PathMatcher.boundary.unit.test.ts b/test/unit/service/PathMatcher/PathMatcher.boundary.unit.test.ts
new file mode 100644
index 0000000..53471b9
--- /dev/null
+++ b/test/unit/service/PathMatcher/PathMatcher.boundary.unit.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "vitest";
+import { PathMatcher } from "src/service";
+
+describe("PathMatcher: Boundary", () => {
+ const matcher = new PathMatcher();
+
+ it("should handle empty strings", () => {
+ expect(matcher.match("", "")).toBe(true);
+ expect(matcher.extractParams("", "")).toEqual({});
+ });
+
+ it("should handle multiple slashes (splitting by / filters empty parts)", () => {
+ expect(matcher.match("//users//", "/users")).toBe(true);
+ });
+
+ it("should handle paths with special characters in params", () => {
+ expect(matcher.extractParams("/users/{id}", "/users/user@domain.com")).toEqual({
+ id: "user@domain.com"
+ });
+ });
+});
diff --git a/test/unit/service/PathMatcher/PathMatcher.negative.unit.test.ts b/test/unit/service/PathMatcher/PathMatcher.negative.unit.test.ts
new file mode 100644
index 0000000..52d90d7
--- /dev/null
+++ b/test/unit/service/PathMatcher/PathMatcher.negative.unit.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from "vitest";
+import { PathMatcher } from "src/service";
+
+describe("PathMatcher: Negative", () => {
+ const matcher = new PathMatcher();
+
+ describe("match", () => {
+ it("should not match if lengths differ", () => {
+ expect(matcher.match("/users", "/users/123")).toBe(false);
+ expect(matcher.match("/users/123", "/users")).toBe(false);
+ });
+
+ it("should not match if static parts differ", () => {
+ expect(matcher.match("/users", "/posts")).toBe(false);
+ expect(matcher.match("/users/{id}", "/posts/123")).toBe(false);
+ });
+ });
+});
diff --git a/test/unit/service/PathMatcher/PathMatcher.positive.unit.test.ts b/test/unit/service/PathMatcher/PathMatcher.positive.unit.test.ts
new file mode 100644
index 0000000..f3d81b9
--- /dev/null
+++ b/test/unit/service/PathMatcher/PathMatcher.positive.unit.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { PathMatcher } from "src/service";
+
+describe("PathMatcher: Positive", () => {
+ const matcher = new PathMatcher();
+
+ describe("match", () => {
+ it("should match simple paths", () => {
+ expect(matcher.match("/users", "/users")).toBe(true);
+ expect(matcher.match("/", "/")).toBe(true);
+ });
+
+ it("should match paths with parameters", () => {
+ expect(matcher.match("/users/{id}", "/users/123")).toBe(true);
+ expect(matcher.match("/users/{id}/posts/{postId}", "/users/1/posts/2")).toBe(true);
+ });
+ });
+
+ describe("extractParams", () => {
+ it("should extract simple parameters", () => {
+ expect(matcher.extractParams("/users/{id}", "/users/123")).toEqual({ id: "123" });
+ });
+
+ it("should extract multiple parameters", () => {
+ expect(matcher.extractParams("/users/{id}/posts/{postId}", "/users/1/posts/2")).toEqual({
+ id: "1",
+ postId: "2"
+ });
+ });
+
+ it("should return empty object if no parameters in template", () => {
+ expect(matcher.extractParams("/users", "/users")).toEqual({});
+ });
+ });
+});
diff --git a/test/unit/service/RequestFactory/RequestFactory.boundary.unit.test.ts b/test/unit/service/RequestFactory/RequestFactory.boundary.unit.test.ts
new file mode 100644
index 0000000..c8e0c35
--- /dev/null
+++ b/test/unit/service/RequestFactory/RequestFactory.boundary.unit.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+import { RequestFactory } from "src/service";
+import { RequestMethod } from "src/domain/enums";
+
+describe("RequestFactory: Boundary", () => {
+ const factory = new RequestFactory();
+
+ it("should handle null/undefined event properties", () => {
+ const event = null as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.GET, event);
+ expect(req.url.pathname).toBe("/");
+ expect(req.headers).toEqual({});
+ });
+
+ it("should handle empty parameter object", () => {
+ const event = { parameter: {} } as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.GET, event);
+ expect(req.url.pathname).toBe("/");
+ });
+
+ it("should override method from parameter if valid", () => {
+ const event = {
+ parameter: {
+ method: "PUT"
+ }
+ } as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.POST, event);
+ expect(req.method).toBe(RequestMethod.PUT);
+ });
+});
diff --git a/test/unit/service/RequestFactory/RequestFactory.negative.unit.test.ts b/test/unit/service/RequestFactory/RequestFactory.negative.unit.test.ts
new file mode 100644
index 0000000..d2fbee0
--- /dev/null
+++ b/test/unit/service/RequestFactory/RequestFactory.negative.unit.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it, vi } from "vitest";
+import { RequestFactory } from "src/service";
+import { RequestMethod } from "src/domain/enums";
+
+describe("RequestFactory: Negative", () => {
+ const factory = new RequestFactory();
+
+ it("should handle invalid JSON headers gracefully", () => {
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const event = {
+ parameter: {
+ headers: "{ invalid json"
+ }
+ } as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.GET, event);
+ expect(req.headers).toEqual({});
+ expect(consoleSpy).toHaveBeenCalled();
+ consoleSpy.mockRestore();
+ });
+
+ it("should handle invalid JSON body gracefully", () => {
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const event = {
+ postData: {
+ type: "application/json",
+ contents: "{ invalid body"
+ },
+ parameter: {
+ headers: JSON.stringify({ "Content-Type": "application/json" })
+ }
+ } as unknown as GoogleAppsScript.Events.DoPost;
+ const req = factory.create(RequestMethod.POST, event);
+ expect(req.body).toBe("{ invalid body");
+ expect(consoleSpy).toHaveBeenCalled();
+ consoleSpy.mockRestore();
+ });
+});
diff --git a/test/unit/service/RequestFactory/RequestFactory.positive.unit.test.ts b/test/unit/service/RequestFactory/RequestFactory.positive.unit.test.ts
new file mode 100644
index 0000000..d84b74f
--- /dev/null
+++ b/test/unit/service/RequestFactory/RequestFactory.positive.unit.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import { RequestFactory } from "src/service";
+import { RequestMethod } from "src/domain/enums";
+
+describe("RequestFactory: Positive", () => {
+ const factory = new RequestFactory();
+
+ it("should create a basic GET request", () => {
+ const event = {
+ parameter: {},
+ parameters: {},
+ queryString: "",
+ pathInfo: "users"
+ } as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.GET, event);
+
+ expect(req.method).toBe(RequestMethod.GET);
+ expect(req.url.pathname).toBe("/users");
+ expect(req.headers).toEqual({});
+ });
+
+ it("should parse JSON headers if provided", () => {
+ const headers = { "Content-Type": "application/json", "X-Custom": "value" };
+ const event = {
+ parameter: {
+ headers: JSON.stringify(headers)
+ }
+ } as unknown as GoogleAppsScript.Events.DoGet;
+ const req = factory.create(RequestMethod.GET, event);
+ expect(req.headers).toEqual(headers);
+ });
+
+ it("should parse JSON body for POST requests", () => {
+ const body = { id: 1, name: "Test" };
+ const event = {
+ postData: {
+ type: "application/json",
+ contents: JSON.stringify(body)
+ },
+ parameter: {
+ headers: JSON.stringify({ "Content-Type": "application/json" })
+ }
+ } as unknown as GoogleAppsScript.Events.DoPost;
+ const req = factory.create(RequestMethod.POST, event);
+ expect(req.body).toEqual(body);
+ });
+});
diff --git a/test/unit/service/Resolver/Resolver.boundary.unit.test.ts b/test/unit/service/Resolver/Resolver.boundary.unit.test.ts
new file mode 100644
index 0000000..98e9f6a
--- /dev/null
+++ b/test/unit/service/Resolver/Resolver.boundary.unit.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { Resolver } from "src/service";
+import { Inject, Injectable } from "src/controller/decorators";
+
+import { InjectionToken } from "src/domain/types";
+
+describe("Resolver: Boundary", () => {
+ it("should handle circular dependencies (currently not supported by simple Resolver, might throw StackOverflow or error)", () => {
+ // Note: Simple Resolver usually fails on circular dependencies.
+ // Testing how it behaves.
+ @Injectable()
+ class _ServiceA {
+ constructor(@Inject("ServiceB") public _b: unknown) {}
+ }
+ @Injectable()
+ class _ServiceB {
+ constructor(@Inject("ServiceA") public _a: unknown) {}
+ }
+
+ const providers = new Map();
+ providers.set("ServiceA", null);
+ providers.set("ServiceB", null);
+ const resolver = new Resolver(new Map(), providers);
+
+ expect(() => resolver.resolve("ServiceA")).toThrow();
+ });
+});
diff --git a/test/unit/service/Resolver/Resolver.extra.unit.test.ts b/test/unit/service/Resolver/Resolver.extra.unit.test.ts
new file mode 100644
index 0000000..b84dd0b
--- /dev/null
+++ b/test/unit/service/Resolver/Resolver.extra.unit.test.ts
@@ -0,0 +1,41 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { Resolver } from "src/service";
+import { PARAMTYPES_METADATA } from "src/domain/constants";
+
+describe("Resolver: Extra", () => {
+ let resolver: Resolver;
+
+ beforeEach(() => {
+ resolver = new Resolver(new Map(), new Map());
+ });
+
+ it("should throw error for invalid injection token (not a function)", () => {
+ class Target {
+ constructor(_dep: unknown) {}
+ }
+ // Simulate invalid metadata
+ Reflect.defineMetadata(PARAMTYPES_METADATA, [undefined], Target);
+
+ // We need to make sure target.length is at least 1, or use explicit inject
+ expect(() => resolver.resolve(Target)).toThrow(
+ "[Resolve ERROR]: Dependency at index 0 of 'Target' cannot be resolved (no token)."
+ );
+
+ Reflect.defineMetadata(PARAMTYPES_METADATA, ["not-a-function"], Target);
+ expect(() => resolver.resolve(Target)).toThrow("Invalid injection token");
+ });
+
+ it("should warn when resolving a class that is neither a controller nor injectable", () => {
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ class RawClass {}
+
+ const instance = resolver.resolve(RawClass);
+
+ expect(instance).toBeInstanceOf(RawClass);
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining("'RawClass' is not registered as a provider or controller")
+ );
+ warnSpy.mockRestore();
+ });
+});
diff --git a/test/unit/service/Resolver/Resolver.negative.unit.test.ts b/test/unit/service/Resolver/Resolver.negative.unit.test.ts
new file mode 100644
index 0000000..358753d
--- /dev/null
+++ b/test/unit/service/Resolver/Resolver.negative.unit.test.ts
@@ -0,0 +1,34 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { Resolver } from "src/service";
+import { Inject, Injectable } from "src/controller/decorators";
+import { InjectionToken } from "src/domain/types";
+
+describe("Resolver: Negative", () => {
+ it("should throw error if dependency is not registered", () => {
+ class UnregisteredService {}
+ @Injectable()
+ class MainService {
+ constructor(@Inject(UnregisteredService) public dep: UnregisteredService) {}
+ }
+
+ const providers = new Map();
+ providers.set(MainService, null);
+ const resolver = new Resolver(new Map(), providers);
+
+ expect(() => resolver.resolve(MainService)).toThrow(/is not registered/);
+ });
+
+ it("should throw error if dependency token is missing (e.g. interface without @Inject)", () => {
+ @Injectable()
+ class MainService {
+ constructor(public _dep: unknown) {} // No type, no @Inject
+ }
+
+ const providers = new Map();
+ providers.set(MainService, null);
+ const resolver = new Resolver(new Map(), providers);
+
+ expect(() => resolver.resolve(MainService)).toThrow(/cannot be resolved \(no token\)/);
+ });
+});
diff --git a/test/unit/service/Resolver/Resolver.positive.unit.test.ts b/test/unit/service/Resolver/Resolver.positive.unit.test.ts
new file mode 100644
index 0000000..d0a06b1
--- /dev/null
+++ b/test/unit/service/Resolver/Resolver.positive.unit.test.ts
@@ -0,0 +1,51 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { Resolver } from "src/service";
+import { Inject, Injectable } from "src/controller/decorators";
+import { InjectionToken } from "src/domain/types";
+
+describe("Resolver: Positive", () => {
+ it("should resolve a simple class without dependencies", () => {
+ @Injectable()
+ class SimpleService {}
+
+ const providers = new Map();
+ providers.set(SimpleService, null);
+ const resolver = new Resolver(new Map(), providers);
+
+ const instance = resolver.resolve(SimpleService);
+ expect(instance).toBeInstanceOf(SimpleService);
+ });
+
+ it("should resolve a class with dependencies", () => {
+ @Injectable()
+ class DepService {}
+
+ @Injectable()
+ class MainService {
+ constructor(@Inject(DepService) public dep: DepService) {}
+ }
+
+ const providers = new Map();
+ providers.set(DepService, null);
+ providers.set(MainService, null);
+ const resolver = new Resolver(new Map(), providers);
+
+ const instance = resolver.resolve(MainService);
+ expect(instance).toBeInstanceOf(MainService);
+ expect(instance.dep).toBeInstanceOf(DepService);
+ });
+
+ it("should resolve a singleton (return same instance)", () => {
+ @Injectable()
+ class SingletonService {}
+
+ const providers = new Map();
+ providers.set(SingletonService, null);
+ const resolver = new Resolver(new Map(), providers);
+
+ const instance1 = resolver.resolve(SingletonService);
+ const instance2 = resolver.resolve(SingletonService);
+ expect(instance1).toBe(instance2);
+ });
+});
diff --git a/test/unit/service/ResponseBuilder/ResponseBuilder.extra.unit.test.ts b/test/unit/service/ResponseBuilder/ResponseBuilder.extra.unit.test.ts
new file mode 100644
index 0000000..351422f
--- /dev/null
+++ b/test/unit/service/ResponseBuilder/ResponseBuilder.extra.unit.test.ts
@@ -0,0 +1,72 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { ResponseBuilder } from "src/service";
+import { HeaderAcceptMimeType, HttpStatus, RequestMethod } from "src/domain/enums";
+import { HttpRequest } from "src/domain/types";
+
+describe("ResponseBuilder: Extra", () => {
+ let builder: ResponseBuilder;
+
+ beforeEach(() => {
+ builder = new ResponseBuilder();
+ (global as unknown as Record).ContentService = {
+ createTextOutput: vi.fn().mockReturnValue({
+ setMimeType: vi.fn().mockReturnThis()
+ }),
+ MimeType: { JSON: "JSON", TEXT: "TEXT" }
+ };
+ (global as unknown as Record).HtmlService = {
+ createHtmlOutput: vi.fn().mockReturnValue("HTML_OUTPUT")
+ };
+ });
+
+ it("should return string for GOOGLE_JSON and GOOGLE_TEXT", () => {
+ const response = { headers: {}, body: { foo: "bar" }, status: 200, ok: true, statusText: "OK" };
+
+ const reqJson = {
+ method: RequestMethod.GET,
+ headers: { Accept: HeaderAcceptMimeType.GOOGLE_JSON },
+ url: {}
+ } as unknown as HttpRequest;
+ expect(builder.wrap(reqJson, response)).toBe(JSON.stringify(response.body));
+
+ const reqText = {
+ method: RequestMethod.GET,
+ headers: { Accept: HeaderAcceptMimeType.GOOGLE_TEXT },
+ url: {}
+ } as unknown as HttpRequest;
+ expect(builder.wrap(reqText, response)).toBe(JSON.stringify(response.body));
+ });
+
+ it("should use ContentService for TEXT mime type", () => {
+ const response = { headers: {}, body: "plain text", status: 200, ok: true, statusText: "OK" };
+ const request = {
+ method: RequestMethod.GET,
+ headers: { Accept: HeaderAcceptMimeType.TEXT },
+ url: {}
+ } as unknown as HttpRequest;
+
+ builder.wrap(request, response);
+ expect(vi.mocked(global.ContentService).createTextOutput).toHaveBeenCalledWith(
+ JSON.stringify(response.body)
+ );
+ });
+
+ it("should use HtmlService as default", () => {
+ const response = { headers: {}, body: "some body", status: 200, ok: true, statusText: "OK" };
+ const request = {
+ method: RequestMethod.GET,
+ headers: { Accept: "unknown/type" },
+ url: {}
+ } as unknown as HttpRequest;
+
+ const result = builder.wrap(request, response);
+ expect(vi.mocked(global.HtmlService).createHtmlOutput).toHaveBeenCalled();
+ expect(result).toBe("HTML_OUTPUT");
+ });
+
+ it("should handle unknown status code in create", () => {
+ const request = { method: RequestMethod.GET } as unknown as HttpRequest;
+ const response = builder.create(request, 999 as unknown as HttpStatus);
+ expect(response.statusText).toBe("UNKNOWN_STATUS");
+ });
+});
diff --git a/test/unit/service/ResponseBuilder/ResponseBuilder.negative.unit.test.ts b/test/unit/service/ResponseBuilder/ResponseBuilder.negative.unit.test.ts
new file mode 100644
index 0000000..58bb758
--- /dev/null
+++ b/test/unit/service/ResponseBuilder/ResponseBuilder.negative.unit.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import { ResponseBuilder } from "src/service";
+import { HttpStatus, RequestMethod } from "src/domain/enums";
+import { HttpRequest } from "src/domain/types";
+
+describe("ResponseBuilder: Negative", () => {
+ const builder = new ResponseBuilder();
+ const mockRequest = {
+ method: RequestMethod.GET,
+ headers: {},
+ url: { pathname: "/" }
+ } as unknown as HttpRequest;
+
+ it("should handle unknown status codes gracefully", () => {
+ const res = builder.create(mockRequest, 999 as unknown as HttpStatus);
+ expect(res.statusText).toBe("UNKNOWN_STATUS");
+ expect(res.status).toBe(999);
+ });
+});
diff --git a/test/unit/service/ResponseBuilder/ResponseBuilder.positive.unit.test.ts b/test/unit/service/ResponseBuilder/ResponseBuilder.positive.unit.test.ts
new file mode 100644
index 0000000..cb821da
--- /dev/null
+++ b/test/unit/service/ResponseBuilder/ResponseBuilder.positive.unit.test.ts
@@ -0,0 +1,70 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { ResponseBuilder } from "src/service";
+import { HeaderAcceptMimeType, HttpStatus, RequestMethod } from "src/domain/enums";
+import { HttpRequest } from "src/domain/types";
+
+describe("ResponseBuilder: Positive", () => {
+ const builder = new ResponseBuilder();
+ const mockRequest = {
+ method: RequestMethod.GET,
+ headers: {},
+ url: { pathname: "/" }
+ } as unknown as HttpRequest;
+
+ beforeEach(() => {
+ vi.stubGlobal("ContentService", {
+ createTextOutput: vi.fn().mockReturnValue({
+ setMimeType: vi.fn().mockReturnThis()
+ }),
+ MimeType: {
+ JSON: "JSON",
+ TEXT: "TEXT"
+ }
+ });
+ vi.stubGlobal("HtmlService", {
+ createHtmlOutput: vi.fn().mockReturnValue("html-output")
+ });
+ });
+
+ describe("create", () => {
+ it("should create a successful response with default status OK for GET", () => {
+ const data = { message: "ok" };
+ const res = builder.create(mockRequest, undefined, {}, data);
+
+ expect(res.status).toBe(HttpStatus.OK);
+ expect(res.ok).toBe(true);
+ expect(res.body).toEqual(data);
+ });
+
+ it("should create a successful response with default status CREATED for POST", () => {
+ const postRequest = { ...mockRequest, method: RequestMethod.POST } as HttpRequest;
+ const res = builder.create(postRequest, undefined, {}, { id: 1 });
+
+ expect(res.status).toBe(HttpStatus.CREATED);
+ });
+
+ it("should handle error status and wrap body in error object", () => {
+ const res = builder.create(mockRequest, HttpStatus.BAD_REQUEST, {}, "invalid");
+ expect(res.status).toBe(HttpStatus.BAD_REQUEST);
+ expect(res.ok).toBe(false);
+ expect(res.body).toEqual({ error: "invalid" });
+ });
+ });
+
+ describe("wrap", () => {
+ it("should wrap as JSON if Accept header is application/json", () => {
+ const req = { ...mockRequest, headers: { Accept: HeaderAcceptMimeType.JSON } } as HttpRequest;
+ const res = builder.create(req, HttpStatus.OK, {}, { foo: "bar" });
+
+ builder.wrap(req, res);
+ expect(vi.mocked(global.ContentService).createTextOutput).toHaveBeenCalled();
+ });
+
+ it("should wrap as HTML by default", () => {
+ const res = builder.create(mockRequest, HttpStatus.OK, {}, "hello");
+ const result = builder.wrap(mockRequest, res);
+ expect(vi.mocked(global.HtmlService).createHtmlOutput).toHaveBeenCalled();
+ expect(result).toBe("html-output");
+ });
+ });
+});
diff --git a/test/unit/service/Router/Router.boundary.unit.test.ts b/test/unit/service/Router/Router.boundary.unit.test.ts
new file mode 100644
index 0000000..3ef1a94
--- /dev/null
+++ b/test/unit/service/Router/Router.boundary.unit.test.ts
@@ -0,0 +1,56 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { Resolver, Router } from "src/service";
+import { HttpStatus, RequestMethod } from "src/domain/enums";
+import { HttpHeaders, HttpRequest, HttpResponse, RouteMetadata } from "src/domain/types";
+
+describe("Router: Boundary", () => {
+ it("should handle routes with same path but different methods", async () => {
+ class TestController {
+ get() {
+ return "get";
+ }
+ post() {
+ return "post";
+ }
+ }
+ const instance = new TestController();
+ const mockResolver = { resolve: vi.fn().mockReturnValue(instance) } as unknown as Resolver;
+ const routes: RouteMetadata[] = [
+ { controller: TestController, handler: "get", method: RequestMethod.GET, path: "/test" },
+ { controller: TestController, handler: "post", method: RequestMethod.POST, path: "/test" }
+ ];
+ const router = new Router(mockResolver, routes);
+ const responseBuilder = vi.fn().mockImplementation((_req, status, headers, data) => ({
+ status: status || HttpStatus.OK,
+ headers: headers || {},
+ body: data,
+ ok: (status || HttpStatus.OK) < 400,
+ statusText: "OK"
+ }));
+
+ const res1 = await router.handle(
+ { method: RequestMethod.GET, url: { pathname: "/test" } } as unknown as HttpRequest,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ responseBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(res1.body).toBe("get");
+
+ const res2 = await router.handle(
+ { method: RequestMethod.POST, url: { pathname: "/test" } } as unknown as HttpRequest,
+ {} as unknown as GoogleAppsScript.Events.DoPost,
+ responseBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(res2.body).toBe("post");
+ });
+});
diff --git a/test/unit/service/Router/Router.extra.unit.test.ts b/test/unit/service/Router/Router.extra.unit.test.ts
new file mode 100644
index 0000000..12affcd
--- /dev/null
+++ b/test/unit/service/Router/Router.extra.unit.test.ts
@@ -0,0 +1,216 @@
+import "reflect-metadata";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { Resolver, Router } from "src/service";
+import { HttpStatus, RequestMethod } from "src/domain/enums";
+import {
+ Body,
+ Event,
+ Headers,
+ Param,
+ Query,
+ Request,
+ Response
+} from "src/controller/decorators/params";
+import { Get } from "src/controller/decorators/routing";
+import { Inject } from "src/controller/decorators";
+import { HttpHeaders, HttpRequest, HttpResponse, RouteMetadata } from "src/domain/types";
+
+describe("Router: Extra", () => {
+ let resolver: Resolver;
+ let router: Router;
+
+ beforeEach(() => {
+ resolver = {
+ resolve: vi.fn()
+ } as unknown as Resolver;
+ });
+
+ it("should return HttpResponse directly if returned from handler", async () => {
+ class TestController {
+ @Get("/")
+ handle() {
+ return { status: 201, body: { done: true }, headers: {}, ok: true, statusText: "Created" };
+ }
+ }
+ const routes: RouteMetadata[] = [
+ { controller: TestController, handler: "handle", method: RequestMethod.GET, path: "/" }
+ ];
+ router = new Router(resolver, routes);
+ vi.mocked(resolver.resolve).mockReturnValue(new TestController());
+
+ const request = { method: RequestMethod.GET, url: { pathname: "/" } } as unknown as HttpRequest;
+ const resBuilder = vi
+ .fn()
+ .mockImplementation((_req, status, _headers, data) => ({ status, body: data }));
+
+ const result = await router.handle(
+ request,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ resBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(result.status).toBe(201);
+ expect((result as unknown as HttpResponse).body).toEqual({ done: true });
+ expect(resBuilder).toHaveBeenCalledTimes(1);
+ });
+
+ it("should inject various parameter types (with and without keys)", async () => {
+ class Service {}
+ const serviceInstance = new Service();
+ class TestController {
+ @Get("/")
+ handle(
+ @Headers() h: unknown,
+ @Headers("X-Custom") custom: string,
+ @Event() e: unknown,
+ @Event("foo") eFoo: string,
+ @Request() req: unknown,
+ @Request("method") reqMethod: string,
+ @Response() res: unknown,
+ @Response("status") resStatus: number,
+ @Inject(Service) s: Service,
+ @Param() p: unknown,
+ @Param("id") pId: string,
+ @Query() q: unknown,
+ @Query("name") qName: string,
+ @Body() b: unknown,
+ @Body("user") bUser: string
+ ) {
+ return {
+ h,
+ custom,
+ e,
+ eFoo,
+ req,
+ reqMethod,
+ res,
+ resStatus,
+ s,
+ p,
+ pId,
+ q,
+ qName,
+ b,
+ bUser
+ };
+ }
+ }
+
+ vi.mocked(resolver.resolve).mockImplementation((target: unknown) => {
+ if (target === TestController) return new TestController();
+ if (target === Service) return serviceInstance;
+ return null;
+ });
+
+ const routes: RouteMetadata[] = [
+ { controller: TestController, handler: "handle", method: RequestMethod.GET, path: "/{id}" }
+ ];
+ router = new Router(resolver, routes);
+
+ const mockEvent = { foo: "bar" };
+ const request = {
+ method: RequestMethod.GET,
+ url: { pathname: "/123", query: { name: "val" } },
+ headers: { "x-custom": "value" },
+ body: { user: "john" }
+ } as unknown as HttpRequest;
+
+ const resBuilder = vi
+ .fn()
+ .mockImplementation((_req, status, headers, data) => ({ status, headers, body: data }));
+
+ const response = await router.handle(
+ request,
+ mockEvent as unknown as GoogleAppsScript.Events.DoGet,
+ resBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ const result = response.body as Record;
+
+ expect(result.h).toEqual(request.headers);
+ expect(result.custom).toBe("value");
+ expect(result.e).toEqual(mockEvent);
+ expect(result.eFoo).toBe("bar");
+ expect(result.req).toEqual(request);
+ expect(result.reqMethod).toBe(RequestMethod.GET);
+ expect(result.res).toBeDefined();
+ expect(result.s).toBe(serviceInstance);
+ expect(result.p).toEqual({ id: "123" });
+ expect(result.pId).toBe("123");
+ expect(result.q).toEqual({ name: "val" });
+ expect(result.qName).toBe("val");
+ expect(result.b).toEqual({ user: "john" });
+ expect(result.bUser).toBe("john");
+ });
+
+ it("should handle missing token in @Inject", async () => {
+ class TestController {
+ @Get("/")
+ handle(@Inject(undefined as unknown as string) s: unknown) {
+ return s;
+ }
+ }
+ const routes: RouteMetadata[] = [
+ { controller: TestController, handler: "handle", method: RequestMethod.GET, path: "/" }
+ ];
+ router = new Router(resolver, routes);
+ vi.mocked(resolver.resolve).mockReturnValue(new TestController());
+ const request = { method: RequestMethod.GET, url: { pathname: "/" } } as unknown as HttpRequest;
+ const resBuilder = vi
+ .fn()
+ .mockImplementation((_req, status, _headers, data) => ({ status, body: data }));
+
+ const response = await router.handle(
+ request,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ resBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(response.body).toBeUndefined();
+ });
+
+ it("should handle injection failure in router", async () => {
+ class TestController {
+ @Get("/")
+ handle(@Inject("UNKNOWN") s: unknown) {
+ return s;
+ }
+ }
+ vi.mocked(resolver.resolve).mockImplementation((target: unknown) => {
+ if (target === TestController) return new TestController();
+ throw new Error("Resolve failed");
+ });
+ const routes: RouteMetadata[] = [
+ { controller: TestController, handler: "handle", method: RequestMethod.GET, path: "/" }
+ ];
+ router = new Router(resolver, routes);
+ const request = { method: RequestMethod.GET, url: { pathname: "/" } } as unknown as HttpRequest;
+ const resBuilder = vi
+ .fn()
+ .mockImplementation((_req, status, _headers, data) => ({ status, body: data }));
+
+ const response = await router.handle(
+ request,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ resBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(response.body).toBeUndefined();
+ });
+});
diff --git a/test/unit/service/Router/Router.negative.unit.test.ts b/test/unit/service/Router/Router.negative.unit.test.ts
new file mode 100644
index 0000000..56ff540
--- /dev/null
+++ b/test/unit/service/Router/Router.negative.unit.test.ts
@@ -0,0 +1,86 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { Resolver, Router } from "src/service";
+import { HttpStatus, RequestMethod } from "src/domain/enums";
+import { HttpHeaders, HttpRequest, HttpResponse, RouteMetadata } from "src/domain/types";
+
+describe("Router: Negative", () => {
+ it("should return 404 if no route matches", async () => {
+ const mockResolver = { resolve: vi.fn() } as unknown as Resolver;
+ const router = new Router(mockResolver, []);
+
+ const mockRequest = {
+ method: RequestMethod.GET,
+ url: { pathname: "/unknown" }
+ } as unknown as HttpRequest;
+
+ const responseBuilder = vi.fn().mockImplementation((_req, status, headers, data) => ({
+ status: status || HttpStatus.OK,
+ headers: headers || {},
+ body: data,
+ ok: (status || HttpStatus.OK) < 400,
+ statusText: status === 404 ? "Not Found" : "Error"
+ }));
+
+ const response = await router.handle(
+ mockRequest,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ responseBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+
+ expect(response.status).toBe(404);
+ expect((response.body as Record).message).toContain("Cannot get /unknown");
+ });
+
+ it("should return 500 if handler throws error", async () => {
+ class TestController {
+ fail() {
+ throw new Error("Boom");
+ }
+ }
+ const controllerInstance = new TestController();
+ const mockResolver = {
+ resolve: vi.fn().mockReturnValue(controllerInstance)
+ } as unknown as Resolver;
+ const routes: RouteMetadata[] = [
+ {
+ controller: TestController,
+ handler: "fail",
+ method: RequestMethod.GET,
+ path: "/fail"
+ }
+ ];
+ const router = new Router(mockResolver, routes);
+
+ const mockRequest = {
+ method: RequestMethod.GET,
+ url: { pathname: "/fail" }
+ } as unknown as HttpRequest;
+
+ const responseBuilder = vi.fn().mockImplementation((_req, status, headers, data) => ({
+ status: status || HttpStatus.OK,
+ headers: headers || {},
+ body: data,
+ ok: (status || HttpStatus.OK) < 400,
+ statusText: "Error"
+ }));
+
+ const response = await router.handle(
+ mockRequest,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ responseBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+ expect(response.status).toBe(500);
+ expect(response.body).toBe("Boom");
+ });
+});
diff --git a/test/unit/service/Router/Router.positive.unit.test.ts b/test/unit/service/Router/Router.positive.unit.test.ts
new file mode 100644
index 0000000..aa596d0
--- /dev/null
+++ b/test/unit/service/Router/Router.positive.unit.test.ts
@@ -0,0 +1,63 @@
+import "reflect-metadata";
+import { describe, expect, it, vi } from "vitest";
+import { Resolver, Router } from "src/service";
+import { HttpStatus, RequestMethod } from "src/domain/enums";
+import { Get } from "src/controller/decorators/routing";
+import { Param } from "src/controller/decorators/params";
+import { HttpController } from "src/controller/decorators";
+import { HttpHeaders, HttpRequest, HttpResponse, RouteMetadata } from "src/domain/types";
+
+describe("Router: Positive", () => {
+ it("should find and call the correct route handler", async () => {
+ @HttpController("/users")
+ class UserController {
+ @Get("/{id}")
+ findOne(@Param("id") id: string) {
+ return { id, name: "User " + id };
+ }
+ }
+
+ const controllerInstance = new UserController();
+ const mockResolver = {
+ resolve: vi.fn().mockReturnValue(controllerInstance)
+ } as unknown as Resolver;
+
+ const routes: RouteMetadata[] = [
+ {
+ controller: UserController,
+ handler: "findOne",
+ method: RequestMethod.GET,
+ path: "/users/{id}"
+ }
+ ];
+
+ const router = new Router(mockResolver, routes);
+
+ const mockRequest = {
+ method: RequestMethod.GET,
+ url: { pathname: "/users/123", query: {} },
+ headers: {}
+ } as unknown as HttpRequest;
+
+ const responseBuilder = vi.fn().mockImplementation((_req, status, headers, data) => ({
+ status: status || 200,
+ headers,
+ body: data
+ }));
+
+ const response = await router.handle(
+ mockRequest,
+ {} as unknown as GoogleAppsScript.Events.DoGet,
+ responseBuilder as unknown as (
+ request: HttpRequest,
+ status?: HttpStatus,
+ headers?: HttpHeaders,
+ data?: unknown
+ ) => HttpResponse
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ id: "123", name: "User 123" });
+ expect(mockResolver.resolve).toHaveBeenCalledWith(UserController);
+ });
+});
diff --git a/test/unit/service/RouterExplorer/RouterExplorer.boundary.unit.test.ts b/test/unit/service/RouterExplorer/RouterExplorer.boundary.unit.test.ts
new file mode 100644
index 0000000..c10372d
--- /dev/null
+++ b/test/unit/service/RouterExplorer/RouterExplorer.boundary.unit.test.ts
@@ -0,0 +1,12 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { RouterExplorer } from "src/service";
+
+describe("RouterExplorer: Boundary", () => {
+ const explorer = new RouterExplorer();
+
+ it("should handle empty controllers map", () => {
+ const routes = explorer.explore(new Map());
+ expect(routes).toHaveLength(0);
+ });
+});
diff --git a/test/unit/service/RouterExplorer/RouterExplorer.negative.unit.test.ts b/test/unit/service/RouterExplorer/RouterExplorer.negative.unit.test.ts
new file mode 100644
index 0000000..ccb7623
--- /dev/null
+++ b/test/unit/service/RouterExplorer/RouterExplorer.negative.unit.test.ts
@@ -0,0 +1,22 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { RouterExplorer } from "src/service";
+import { HttpController } from "src/controller/decorators";
+import { Newable } from "src/domain/types";
+
+describe("RouterExplorer: Negative", () => {
+ const explorer = new RouterExplorer();
+
+ it("should ignore controllers that are not 'http' type", () => {
+ @HttpController("other")
+ class OtherController {
+ test() {}
+ }
+
+ const controllers = new Map();
+ controllers.set(OtherController, null);
+
+ const routes = explorer.explore(controllers);
+ expect(routes).toHaveLength(0);
+ });
+});
diff --git a/test/unit/service/RouterExplorer/RouterExplorer.positive.unit.test.ts b/test/unit/service/RouterExplorer/RouterExplorer.positive.unit.test.ts
new file mode 100644
index 0000000..336418e
--- /dev/null
+++ b/test/unit/service/RouterExplorer/RouterExplorer.positive.unit.test.ts
@@ -0,0 +1,43 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { RouterExplorer } from "src/service";
+import { Get, Post } from "src/controller/decorators/routing";
+import { HttpController } from "src/controller/decorators";
+import { RequestMethod } from "src/domain/enums";
+import { Newable } from "src/domain/types";
+
+describe("RouterExplorer: Positive", () => {
+ const explorer = new RouterExplorer();
+
+ it("should explore routes from an HttpController", () => {
+ @HttpController("/users")
+ class UserController {
+ @Get()
+ findAll() {}
+
+ @Post("/{id}")
+ create() {}
+ }
+
+ const controllers = new Map();
+ controllers.set(UserController, null);
+
+ const routes = explorer.explore(controllers);
+
+ expect(routes).toHaveLength(2);
+ expect(routes).toContainEqual(
+ expect.objectContaining({
+ path: "/users",
+ method: RequestMethod.GET,
+ handler: "findAll"
+ })
+ );
+ expect(routes).toContainEqual(
+ expect.objectContaining({
+ path: "/users/{id}",
+ method: RequestMethod.POST,
+ handler: "create"
+ })
+ );
+ });
+});
diff --git a/test/unit/service/Services.extra.unit.test.ts b/test/unit/service/Services.extra.unit.test.ts
new file mode 100644
index 0000000..7ad7016
--- /dev/null
+++ b/test/unit/service/Services.extra.unit.test.ts
@@ -0,0 +1,45 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { RequestFactory, RouterExplorer } from "src/service";
+import { Get } from "src/controller/decorators/routing";
+import { RequestMethod } from "src/domain/enums";
+import { HttpController } from "src/controller/decorators";
+
+describe("Services: Extra Coverage (RequestFactory & RouterExplorer)", () => {
+ describe("RequestFactory", () => {
+ it("should fallback to postData.type if Content-Type header is missing", () => {
+ const factory = new RequestFactory();
+ const event = {
+ parameter: {},
+ postData: {
+ contents: JSON.stringify({ foo: "bar" }),
+ type: "application/json"
+ }
+ } as unknown;
+
+ const request = factory.create(
+ RequestMethod.POST,
+ event as unknown as GoogleAppsScript.Events.DoPost
+ );
+ expect(request.body).toEqual({ foo: "bar" });
+ });
+ });
+
+ describe("RouterExplorer", () => {
+ it("should use default basePath and handle methods without metadata", () => {
+ @HttpController() // No path provided, should default to "/"
+ class TestController {
+ methodWithoutDecorator() {}
+
+ @Get("/test")
+ testMethod() {}
+ }
+
+ const explorer = new RouterExplorer();
+ const routes = explorer.explore(new Map([[TestController, null]]));
+
+ expect(routes.length).toBe(1);
+ expect(routes[0].path).toBe("/test");
+ });
+ });
+});
diff --git a/test/unit/shared/utils/isController/isController.boundary.unit.test.ts b/test/unit/shared/utils/isController/isController.boundary.unit.test.ts
new file mode 100644
index 0000000..716a45d
--- /dev/null
+++ b/test/unit/shared/utils/isController/isController.boundary.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isController } from "src/shared/utils";
+import { Newable } from "src/domain/types";
+
+describe("isController: Boundary", () => {
+ it("should handle null/undefined", () => {
+ expect(() => isController(null as unknown as Newable)).toThrow();
+ });
+});
diff --git a/test/unit/shared/utils/isController/isController.negative.unit.test.ts b/test/unit/shared/utils/isController/isController.negative.unit.test.ts
new file mode 100644
index 0000000..ec8535b
--- /dev/null
+++ b/test/unit/shared/utils/isController/isController.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isController } from "src/shared/utils";
+
+describe("isController: Negative", () => {
+ it("should return false for a class without decorator", () => {
+ class NormalClass {}
+ expect(isController(NormalClass)).toBe(false);
+ });
+});
diff --git a/test/unit/shared/utils/isController/isController.positive.unit.test.ts b/test/unit/shared/utils/isController/isController.positive.unit.test.ts
new file mode 100644
index 0000000..7c9a5fc
--- /dev/null
+++ b/test/unit/shared/utils/isController/isController.positive.unit.test.ts
@@ -0,0 +1,12 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isController } from "src/shared/utils";
+import { HttpController } from "src/controller/decorators";
+
+describe("isController: Positive", () => {
+ it("should return true for a class decorated with @HttpController", () => {
+ @HttpController()
+ class TestController {}
+ expect(isController(TestController)).toBe(true);
+ });
+});
diff --git a/test/unit/shared/utils/isInjectable/isInjectable.boundary.unit.test.ts b/test/unit/shared/utils/isInjectable/isInjectable.boundary.unit.test.ts
new file mode 100644
index 0000000..db5e32c
--- /dev/null
+++ b/test/unit/shared/utils/isInjectable/isInjectable.boundary.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isInjectable } from "src/shared/utils";
+import { Newable } from "src/domain/types";
+
+describe("isInjectable: Boundary", () => {
+ it("should handle null/undefined", () => {
+ expect(() => isInjectable(null as unknown as Newable)).toThrow();
+ });
+});
diff --git a/test/unit/shared/utils/isInjectable/isInjectable.negative.unit.test.ts b/test/unit/shared/utils/isInjectable/isInjectable.negative.unit.test.ts
new file mode 100644
index 0000000..f256864
--- /dev/null
+++ b/test/unit/shared/utils/isInjectable/isInjectable.negative.unit.test.ts
@@ -0,0 +1,10 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isInjectable } from "src/shared/utils";
+
+describe("isInjectable: Negative", () => {
+ it("should return false for a class without decorator", () => {
+ class NormalClass {}
+ expect(isInjectable(NormalClass)).toBe(false);
+ });
+});
diff --git a/test/unit/shared/utils/isInjectable/isInjectable.positive.unit.test.ts b/test/unit/shared/utils/isInjectable/isInjectable.positive.unit.test.ts
new file mode 100644
index 0000000..b4b0a01
--- /dev/null
+++ b/test/unit/shared/utils/isInjectable/isInjectable.positive.unit.test.ts
@@ -0,0 +1,24 @@
+import "reflect-metadata";
+import { describe, expect, it } from "vitest";
+import { isInjectable } from "src/shared/utils";
+import { Injectable, Repository, Service } from "src/controller/decorators";
+
+describe("isInjectable: Positive", () => {
+ it("should return true for @Injectable", () => {
+ @Injectable()
+ class TestClass {}
+ expect(isInjectable(TestClass)).toBe(true);
+ });
+
+ it("should return true for @Service", () => {
+ @Service()
+ class TestClass {}
+ expect(isInjectable(TestClass)).toBe(true);
+ });
+
+ it("should return true for @Repository", () => {
+ @Repository()
+ class TestClass {}
+ expect(isInjectable(TestClass)).toBe(true);
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index bf84608..a46800b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,35 @@
-// https://typescriptlang.org/docs/handbook/tsconfig-json.html
{
+ /**
+ * Instructs the TypeScript compiler how to compile the .ts files.
+ * @see https://www.typescriptlang.org/tsconfig#compilerOptions
+ */
+ "compilerOptions": {
+ /**
+ * Emit design-type metadata for decorated declarations in source files.
+ * @see https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata
+ */
+ "emitDecoratorMetadata": true,
+
+ /**
+ * Enable experimental support for legacy experimental decorators.
+ * @see https://www.typescriptlang.org/tsconfig#experimentalDecorators
+ */
+ "experimentalDecorators": true
+ },
+
+ /**
+ * Specifies an allowed list of files to be included in the program.
+ * @see https://www.typescriptlang.org/tsconfig#files
+ */
"files": [],
+
+ /**
+ * Project references are a way to structure your TypeScript programs into smaller pieces.
+ * @see https://www.typescriptlang.org/tsconfig#references
+ */
"references": [
{
- "path": "./config/typescript/tsconfig.json"
+ "path": "./config/typescript/tsconfig.appsscript.json"
},
{
"path": "./config/typescript/tsconfig.node.json"
diff --git a/vitest.config.ts b/vitest.config.ts
index a8aab1e..6d8c29d 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,47 +1,30 @@
-import { readFileSync } from "node:fs";
-import { join, resolve } from "path";
import { defineConfig } from "vitest/config";
-import { BuildOptions, BuildPaths, buildResolversConfig } from "./config/vite";
+import { getAppVersion, getBuildOptions, resolveConfig, testConfig } from "./config/vite";
-// https://vite.dev/config/
-export default defineConfig(
- async ({ command, mode, isSsrBuild, isPreview }) => {
- const rootDir = process.cwd();
- const paths: BuildPaths = {
- root: rootDir,
- config: join(rootDir, "config"),
- dist: resolve(rootDir, "dist"),
- src: join(rootDir, "src")
- };
+/**
+ * Vitest configuration.
+ * @see https://vitest.dev/config/
+ */
+export default defineConfig(async (env) => {
+ const options = getBuildOptions(env);
+ const appVersion = getAppVersion(options.paths.root);
- const options: BuildOptions = {
- paths,
- command,
- mode,
- isSsrBuild,
- isPreview,
- isDev: mode === "development"
- };
+ return {
+ /**
+ * @see https://vite.dev/config/shared-options.html#define
+ */
+ define: {
+ "import.meta.env.APP_VERSION": JSON.stringify(appVersion)
+ },
- let appVersion = null;
+ /**
+ * @see https://vite.dev/config/shared-options.html#resolve
+ */
+ resolve: resolveConfig(options),
- try {
- const packageJsonPath = resolve(rootDir, "package.json");
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
- appVersion = packageJson.version;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- } catch (err: unknown) {
- /* empty */
- }
-
- return {
- define: {
- "import.meta.env.APP_VERSION": JSON.stringify(appVersion)
- },
- resolve: buildResolversConfig(options),
- test: {
- globals: true
- }
- };
- }
-);
+ /**
+ * @see https://vitest.dev/config/
+ */
+ test: testConfig(options)
+ };
+});