From 3c5da7beaad9f308a8ce7590dc02621c9e12572e Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Sun, 7 Dec 2025 12:26:43 +0100 Subject: [PATCH 01/17] audit --- package-lock.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b0eb71..ab8a6c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,11 @@ "eslint-plugin-googleappsscript": "^1.0.5", "husky": "^9.1.7", "prettier": "^3.6.2", - "typescript": "^5.9.3", "typescript-eslint": "^8.46.2", "vitest": "^4.0.4" + }, + "peerDependencies": { + "typescript": "^5.9.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -2481,9 +2483,9 @@ "license": "ISC" }, "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": { @@ -4024,7 +4026,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": { From 71435230b2717cbd4fdd06889de22f202db6988d Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:33:20 +0100 Subject: [PATCH 02/17] chore: update documentation and base configuration --- .gitattributes | 67 +++ .gitignore | 104 +++- .idea/modules.xml | 2 +- .idea/prettier.xml | 1 + .idea/scopes/appsscript.xml | 2 +- .idea/scopes/stories.xml | 2 +- .idea/scopes/webapp.xml | 2 +- .idea/webResources.xml | 14 - .prettierignore | 45 +- .prettierrc.cjs | 56 ++ CODE_OF_CONDUCT.md | 76 +++ CONTRIBUTING.md | 130 +++++ README.md | 514 +++++++++++------- ROADMAP.md | 49 ++ SECURITY.md | 77 +++ config/eslint/rules-jsdoc.ts | 42 ++ config/eslint/rules-spacing.ts | 49 ++ ...psscript.d.ts => eslint-plugin-jsdoc.d.ts} | 4 +- scripts/maintenance.sh | 135 ----- src/controllers/decorators/RestController.ts | 3 + src/controllers/decorators/index.ts | 55 ++ .../decorators/params/PathVariable.ts | 3 + .../decorators/params/RequestBody.ts | 3 + .../decorators/params/RequestParam.ts | 3 + src/controllers/decorators/params/index.ts | 8 + src/controllers/decorators/routing/Delete.ts | 0 .../decorators/routing/DeleteMapping.ts | 0 .../decorators/routing/GetMapping.ts | 0 src/controllers/decorators/routing/Head.ts | 0 .../decorators/routing/HeadMapping.ts | 0 src/controllers/decorators/routing/Options.ts | 0 src/controllers/decorators/routing/Patch.ts | 0 .../decorators/routing/PatchMapping.ts | 0 src/controllers/decorators/routing/Post.ts | 0 .../decorators/routing/PostMapping.ts | 0 src/controllers/decorators/routing/Put.ts | 0 .../decorators/routing/PutMapping.ts | 0 src/controllers/decorators/routing/index.ts | 0 src/controllers/decorators/security/index.ts | 7 + .../decorators/validation/index.ts | 0 src/controllers/index.ts | 2 + src/domain/constants/index.ts | 0 src/domain/entities/RouteExecutionContext.ts | 0 src/domain/entities/index.ts | 3 + src/domain/enums/index.ts | 4 + src/domain/index.ts | 3 + src/domain/types/InjectTokenDefinition.ts | 0 src/domain/types/ParsedUrl.ts | 10 + src/domain/types/ParsedUrlQuery.ts | 0 src/domain/types/index.ts | 5 + src/exceptions/index.ts | 0 src/repository/assignInjectMetadata.ts | 0 src/repository/assignParamMetadata.ts | 22 + src/repository/index.ts | 3 + src/services/index.ts | 52 ++ 55 files changed, 1198 insertions(+), 359 deletions(-) create mode 100644 .gitattributes delete mode 100644 .idea/webResources.xml create mode 100644 .prettierrc.cjs create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 ROADMAP.md create mode 100644 SECURITY.md create mode 100644 config/eslint/rules-jsdoc.ts create mode 100644 config/eslint/rules-spacing.ts rename config/types/{eslint-plugin-googleappsscript.d.ts => eslint-plugin-jsdoc.d.ts} (56%) delete mode 100644 scripts/maintenance.sh create mode 100644 src/controllers/decorators/RestController.ts create mode 100644 src/controllers/decorators/index.ts create mode 100644 src/controllers/decorators/params/PathVariable.ts create mode 100644 src/controllers/decorators/params/RequestBody.ts create mode 100644 src/controllers/decorators/params/RequestParam.ts create mode 100644 src/controllers/decorators/params/index.ts create mode 100644 src/controllers/decorators/routing/Delete.ts create mode 100644 src/controllers/decorators/routing/DeleteMapping.ts create mode 100644 src/controllers/decorators/routing/GetMapping.ts create mode 100644 src/controllers/decorators/routing/Head.ts create mode 100644 src/controllers/decorators/routing/HeadMapping.ts create mode 100644 src/controllers/decorators/routing/Options.ts create mode 100644 src/controllers/decorators/routing/Patch.ts create mode 100644 src/controllers/decorators/routing/PatchMapping.ts create mode 100644 src/controllers/decorators/routing/Post.ts create mode 100644 src/controllers/decorators/routing/PostMapping.ts create mode 100644 src/controllers/decorators/routing/Put.ts create mode 100644 src/controllers/decorators/routing/PutMapping.ts create mode 100644 src/controllers/decorators/routing/index.ts create mode 100644 src/controllers/decorators/security/index.ts create mode 100644 src/controllers/decorators/validation/index.ts create mode 100644 src/controllers/index.ts create mode 100644 src/domain/constants/index.ts create mode 100644 src/domain/entities/RouteExecutionContext.ts create mode 100644 src/domain/entities/index.ts create mode 100644 src/domain/enums/index.ts create mode 100644 src/domain/index.ts create mode 100644 src/domain/types/InjectTokenDefinition.ts create mode 100644 src/domain/types/ParsedUrl.ts create mode 100644 src/domain/types/ParsedUrlQuery.ts create mode 100644 src/domain/types/index.ts create mode 100644 src/exceptions/index.ts create mode 100644 src/repository/assignInjectMetadata.ts create mode 100644 src/repository/assignParamMetadata.ts create mode 100644 src/repository/index.ts create mode 100644 src/services/index.ts 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/.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/.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/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..1d20944 100644 --- a/README.md +++ b/README.md @@ -8,29 +8,38 @@

-# Boot Framework for Google Apps Script™ projects - -[![Built%20with-clasp](https://img.shields.io/badge/Built%20with-clasp-4285f4.svg)](https://github.com/google/clasp) -[![License](https://img.shields.io/github/license/bootgs/boot?label=License)](LICENSE) -[![Latest release](https://img.shields.io/github/v/release/bootgs/boot?label=Release)](https://github.com/bootgs/boot/releases) +# Boot Framework for Google Apps Script™ + +

+ Built with clasp + License + Security Policy + Roadmap + Latest release +

-[![GitHub Stars](https://img.shields.io/github/stars/bootgs/boot?style=social)](https://github.com/bootgs/boot/stargazers) -[![GitHub Fork](https://img.shields.io/github/forks/bootgs/boot?style=social)](https://github.com/bootgs/boot/forks) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/MaksymStoianov?style=social&logo=github)](https://github.com/sponsors/MaksymStoianov) +

+ GitHub Stars + GitHub Fork + GitHub Sponsors +

## Introduction -**Boot.gs** is a powerful, scalable, and modern framework for building high-performance Google Apps Script applications. +**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. -## How to Install +## Installation -To get started, install the dependencies: +Install the framework via npm: ```bash npm install github:bootgs/boot#main ``` -> **Note:** It's recommended to use tags (`#vX.Y.Z`) for production environments to ensure version stability. +> [!TIP] +> Use specific tags (e.g., `#v1.1.0`) in production for stability. For example: @@ -38,148 +47,64 @@ For example: npm install github:bootgs/boot#v1.1.0 ``` -## 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"; @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 "boot"; +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: - -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. +## Features -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 +112,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()`. | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DecoratorReturnsDescription
@Controller(type?: string, options?: object)ClassDecoratorMarks a class as a general-purpose controller.
@HttpController(basePath?: string)ClassDecoratorMarks a class as an HTTP request controller. Default base path is /.
@SheetController(sheetName?: string | string[] | RegExp)ClassDecoratorMarks a class as a Google Sheets event controller. Can be filtered by sheet name (string, array, or RegExp).
@DocController()ClassDecoratorMarks a class as a Google Docs event controller.
@SlideController()ClassDecoratorMarks a class as a Google Slides event controller.
@FormController()ClassDecoratorMarks a class as a Google Forms event controller.
@Service()ClassDecoratorMarks a class as a service, typically holding business logic.
@Repository()ClassDecoratorMarks a class as a repository, abstracting data access logic.
@Injectable()ClassDecoratorMarks a class as available for dependency injection.
Aliases
@RestController(basePath?: string)ClassDecoratorAlias for @HttpController().
@SheetsController(sheetName?: string | string[] | RegExp)ClassDecoratorAlias for @SheetController().
@DocsController()ClassDecoratorAlias for @DocController().
@SlidesController()ClassDecoratorAlias for @SlideController().
@FormsController()ClassDecoratorAlias for @FormController().
@@ -210,28 +203,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()`. | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DecoratorReturnsDescription
@Install()MethodDecoratorHandles onInstall event.
@Open()MethodDecoratorHandles onOpen event.
@Edit(...range?: (string | RegExp | string[])[])MethodDecoratorHandles onEdit event. Filter by A1-notation, sheet name, or RegExp.
@Change(changeType?: SheetsOnChangeChangeType | SheetsOnChangeChangeType[])MethodDecoratorHandles onChange event. Filter by SheetsOnChangeChangeType.
@SelectionChange()MethodDecoratorHandles onSelectionChange event.
@FormSubmit(...formId?: (string | string[])[])MethodDecoratorHandles onFormSubmit event. Filter by one or more form IDs.
HTTP Methods
@Get(path?: string)MethodDecoratorMaps a method to handle HTTP GET requests. Default path is /.
@Post(path?: string)MethodDecoratorMaps a method to handle HTTP POST requests.
@Put(path?: string)MethodDecoratorMaps a method to handle HTTP PUT requests.
@Patch(path?: string)MethodDecoratorMaps a method to handle HTTP PATCH requests.
@Delete(path?: string)MethodDecoratorMaps a method to handle HTTP DELETE requests.
@Head(path?: string)MethodDecoratorMaps a method to handle HTTP HEAD requests.
@Options(path?: string)MethodDecoratorMaps a method to handle HTTP OPTIONS requests.
Aliases
@GetMapping(path?: string)MethodDecoratorAlias for @Get().
@PostMapping(path?: string)MethodDecoratorAlias for @Post().
@PutMapping(path?: string)MethodDecoratorAlias for @Put().
@PatchMapping(path?: string)MethodDecoratorAlias for @Patch().
@DeleteMapping(path?: string)MethodDecoratorAlias for @Delete().
@HeadMapping(path?: string)MethodDecoratorAlias for @Head().
@OptionsMapping(path?: string)MethodDecoratorAlias for @Options().
@@ -239,38 +327,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. | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DecoratorReturnsDescription
@Event()ParameterDecoratorInjects the full Google Apps Script event object.
@Request(key?: string)ParameterDecoratorInjects the full request object or a specific property.
@Headers(key?: string)ParameterDecoratorInjects request headers or a specific header value.
@Body(key?: string)ParameterDecoratorInjects the full request body or a specific key.
@Param(key?: string)ParameterDecoratorInjects values from URL path parameters.
@Query(key?: string)ParameterDecoratorInjects values from URL query parameters.
@Inject(token: any)ParameterDecoratorExplicitly specifies an injection token for a dependency.
Aliases
@RequestBody(key?: string)ParameterDecoratorAlias for @Body().
@PathVariable(key?: string)ParameterDecoratorAlias for @Param().
@RequestParam(key?: string)ParameterDecoratorAlias 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: + + + + + + + + + + + + + + + + + + +
VersionSupported
>= 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/rules-jsdoc.ts b/config/eslint/rules-jsdoc.ts new file mode 100644 index 0000000..5de8d56 --- /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-spacing.ts b/config/eslint/rules-spacing.ts new file mode 100644 index 0000000..f5f1cf2 --- /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/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/src/controllers/decorators/RestController.ts b/src/controllers/decorators/RestController.ts new file mode 100644 index 0000000..18b2a6a --- /dev/null +++ b/src/controllers/decorators/RestController.ts @@ -0,0 +1,3 @@ +import { HttpController } from "./HttpController"; + +export const RestController = HttpController; diff --git a/src/controllers/decorators/index.ts b/src/controllers/decorators/index.ts new file mode 100644 index 0000000..4d35e01 --- /dev/null +++ b/src/controllers/decorators/index.ts @@ -0,0 +1,55 @@ +// Core +export { BootApplication } from "./controllers/main/application"; +export { ApplicationFactory } from "./controllers/main/application-factory"; + +// Services +export { Resolver } from "./services/resolver"; +export { Inject } from "./services/inject.decorator"; +export { EventDispatcher } from "./services/event-dispatcher"; +export { Service } from "./services/service.decorator"; + +// Decorators +export { Injectable } from "./controllers/decorators/Injectable"; +export { + HttpController, + RestController +} from "./controllers/decorators/routing/http-controller.decorator"; +export { Get } from "./controllers/decorators/routing/get.decorator"; +export { Controller } from "./controllers/decorators/routing/controller.decorator"; +export { Repository } from "./controllers/decorators/routing/repository.decorator"; + +export { + Post, + Put, + Delete, + Patch, + Head, + Options, + GetMapping, + PostMapping, + PutMapping, + DeleteMapping, + PatchMapping, + HeadMapping, + OptionsMapping +} from "./controllers/decorators/routing/methods.decorator"; + +export { Param, PathVariable } from "./controllers/decorators/params/Param"; +export { Query, RequestParam } from "./controllers/decorators/params/Query"; +export { Body, RequestBody } from "./controllers/decorators/params/Body"; + +// Domain +export { Entity } from "./controllers/decorators/Entity"; +export { Newable } from "./domain/types/newable.type"; +export { Provider } from "./domain/types/provider.type"; +export { ApplicationConfig } from "./domain/types/application-config.interface"; +export { RequestMethod } from "./domain/enums/request-method.enum"; +export { HttpStatus } from "./domain/enums/http-status.enum"; +export { AppsScriptEventType } from "./domain/enums/apps-script-event-type.enum"; + +// Repository +export { MetadataRepository } from "./repository/MetadataRepository"; + +// Exceptions +export { AppException } from "./exceptions/app.exception"; +export { HttpException } from "./exceptions/http.exception"; diff --git a/src/controllers/decorators/params/PathVariable.ts b/src/controllers/decorators/params/PathVariable.ts new file mode 100644 index 0000000..06b4355 --- /dev/null +++ b/src/controllers/decorators/params/PathVariable.ts @@ -0,0 +1,3 @@ +import { Param } from "./Param"; + +export const PathVariable = Param; diff --git a/src/controllers/decorators/params/RequestBody.ts b/src/controllers/decorators/params/RequestBody.ts new file mode 100644 index 0000000..48e650f --- /dev/null +++ b/src/controllers/decorators/params/RequestBody.ts @@ -0,0 +1,3 @@ +import { Body } from "./Body"; + +export const RequestBody = Body; diff --git a/src/controllers/decorators/params/RequestParam.ts b/src/controllers/decorators/params/RequestParam.ts new file mode 100644 index 0000000..b72b11a --- /dev/null +++ b/src/controllers/decorators/params/RequestParam.ts @@ -0,0 +1,3 @@ +import { Query } from "./Query"; + +export const RequestParam = Query; diff --git a/src/controllers/decorators/params/index.ts b/src/controllers/decorators/params/index.ts new file mode 100644 index 0000000..16cfdf0 --- /dev/null +++ b/src/controllers/decorators/params/index.ts @@ -0,0 +1,8 @@ +export * from "./Body"; +export * from "./RequestBody"; + +export * from "./Param"; +export * from "./PathVariable"; + +export * from "./Query"; +export * from "./RequestParam"; diff --git a/src/controllers/decorators/routing/Delete.ts b/src/controllers/decorators/routing/Delete.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/DeleteMapping.ts b/src/controllers/decorators/routing/DeleteMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/GetMapping.ts b/src/controllers/decorators/routing/GetMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/Head.ts b/src/controllers/decorators/routing/Head.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/HeadMapping.ts b/src/controllers/decorators/routing/HeadMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/Options.ts b/src/controllers/decorators/routing/Options.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/Patch.ts b/src/controllers/decorators/routing/Patch.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/PatchMapping.ts b/src/controllers/decorators/routing/PatchMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/Post.ts b/src/controllers/decorators/routing/Post.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/PostMapping.ts b/src/controllers/decorators/routing/PostMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/Put.ts b/src/controllers/decorators/routing/Put.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/PutMapping.ts b/src/controllers/decorators/routing/PutMapping.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/routing/index.ts b/src/controllers/decorators/routing/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/decorators/security/index.ts b/src/controllers/decorators/security/index.ts new file mode 100644 index 0000000..4f29d8a --- /dev/null +++ b/src/controllers/decorators/security/index.ts @@ -0,0 +1,7 @@ +export * from "./params"; +export * from "./routing"; +export * from "./security"; +export * from "./validation"; + +export * from "./Entity"; +export * from "./Injectable"; diff --git a/src/controllers/decorators/validation/index.ts b/src/controllers/decorators/validation/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 0000000..0ec57e6 --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1,2 @@ +export * from "./AppException"; +export * from "./HttpException"; diff --git a/src/domain/constants/index.ts b/src/domain/constants/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/entities/RouteExecutionContext.ts b/src/domain/entities/RouteExecutionContext.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/entities/index.ts b/src/domain/entities/index.ts new file mode 100644 index 0000000..e42be83 --- /dev/null +++ b/src/domain/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./constants"; +export * from "./enums"; +export * from "./types"; diff --git a/src/domain/enums/index.ts b/src/domain/enums/index.ts new file mode 100644 index 0000000..51b1446 --- /dev/null +++ b/src/domain/enums/index.ts @@ -0,0 +1,4 @@ +export * from "./constants"; +export * from "./entities"; +export * from "./enums"; +export * from "./types"; diff --git a/src/domain/index.ts b/src/domain/index.ts new file mode 100644 index 0000000..5da7f59 --- /dev/null +++ b/src/domain/index.ts @@ -0,0 +1,3 @@ +export * from "./events.constants"; +export * from "./http.constants"; +export * from "./metadata.constants"; diff --git a/src/domain/types/InjectTokenDefinition.ts b/src/domain/types/InjectTokenDefinition.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/types/ParsedUrl.ts b/src/domain/types/ParsedUrl.ts new file mode 100644 index 0000000..fdf0b98 --- /dev/null +++ b/src/domain/types/ParsedUrl.ts @@ -0,0 +1,10 @@ +export interface ParsedUrlQuery { + [key: string]: string | string[] | undefined; +} + +export interface ParsedUrl { + pathname: string; + path: string; + search?: string; + query: ParsedUrlQuery; +} diff --git a/src/domain/types/ParsedUrlQuery.ts b/src/domain/types/ParsedUrlQuery.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/types/index.ts b/src/domain/types/index.ts new file mode 100644 index 0000000..f3ffb85 --- /dev/null +++ b/src/domain/types/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/exceptions/index.ts b/src/exceptions/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repository/assignInjectMetadata.ts b/src/repository/assignInjectMetadata.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repository/assignParamMetadata.ts b/src/repository/assignParamMetadata.ts new file mode 100644 index 0000000..7acef37 --- /dev/null +++ b/src/repository/assignParamMetadata.ts @@ -0,0 +1,22 @@ +import { ParamDefinition, ParamSource } from "../../../domain"; + +/** + * Updates parameter metadata with the argument's position (index). + * + * @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. + * @returns The updated parameter metadata. + */ +export function assignParamMetadata( + existing: Record, + index: number, + type: ParamSource, + key?: string +): Record { + return { + ...existing, + [`${type as string}:${index}`]: { type, key, index } + }; +} diff --git a/src/repository/index.ts b/src/repository/index.ts new file mode 100644 index 0000000..e42be83 --- /dev/null +++ b/src/repository/index.ts @@ -0,0 +1,3 @@ +export * from "./constants"; +export * from "./enums"; +export * from "./types"; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..64ef6c4 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,52 @@ +// Core +export { BootApplication } from "./controllers/BootApplication"; +export { BootApplicationFactory } from "./controllers/BootApplicationFactory"; + +// Services +export { Resolver } from "./services/Resolver"; +export { Inject } from "./controllers/decorators/Inject"; +export { EventDispatcher } from "./services/EventDispatcher"; +export { Service } from "./controllers/decorators/Service"; + +// Decorators +export { Injectable } from "./controllers/decorators/Injectable"; +export { HttpController, RestController } from "./controllers/decorators/HttpController"; +export { Get } from "./controllers/decorators/routing/Get"; +export { Controller } from "./controllers/decorators/routing/controller.decorator"; +export { Repository } from "./controllers/decorators/Repository"; + +export { + Post, + Put, + Delete, + Patch, + Head, + Options, + GetMapping, + PostMapping, + PutMapping, + DeleteMapping, + PatchMapping, + HeadMapping, + OptionsMapping +} from "./controllers/decorators/routing/OptionsMapping"; + +export { Param, PathVariable } from "./controllers/decorators/params/Param"; +export { Query, RequestParam } from "./controllers/decorators/params/Query"; +export { Body, RequestBody } from "./controllers/decorators/params/Body"; + +// Domain +export { Entity } from "./controllers/decorators/Entity"; +export { Newable } from "./domain/types/newable.type"; +export { Provider } from "./domain/types/provider.type"; +export { ApplicationConfig } from "./domain/types/application-config.interface"; +export { RequestMethod } from "./domain/enums/request-method.enum"; +export { HttpStatus } from "./domain/enums/http-status.enum"; +export { AppsScriptEventType } from "./domain/enums/apps-script-event-type.enum"; + +// Repository +export { MetadataRepository } from "./repository/MetadataRepository"; + +// Exceptions +export { AppException } from "./exceptions/AppException"; +export { HttpException } from "./exceptions/HttpException"; From 67973e98af0d75fbec6cb1b1bd031b9c3fb0f44d Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:33:30 +0100 Subject: [PATCH 03/17] chore: update build and lint configuration --- .prettierrc.json | 10 - config/eslint/base.ts | 7 - config/eslint/common-ignores.ts | 12 + config/eslint/custom-rules.ts | 8 - config/eslint/env-appsscript.ts | 44 + config/eslint/globals.ts | 14 - config/eslint/ignores.ts | 3 - config/eslint/index.ts | 26 + config/eslint/json.ts | 22 - config/eslint/lang-javascript.ts | 14 + config/eslint/lang-json.ts | 28 + config/eslint/lang-markdown.ts | 22 + config/eslint/lang-typescript.ts | 51 + config/eslint/markdown.ts | 8 - config/eslint/overrides-tests.ts | 23 + config/eslint/prettier.ts | 3 - config/eslint/rules-prettier.ts | 12 + config/typescript/tsconfig.appsscript.json | 96 ++ config/typescript/tsconfig.base.json | 95 ++ config/typescript/tsconfig.json | 36 - config/typescript/tsconfig.node.json | 61 +- config/vite/buildAliases.ts | 26 - config/vite/buildResolversConfig.ts | 14 - config/vite/factories/index.ts | 2 + config/vite/factories/resolveConfig.ts | 21 + config/vite/factories/testConfig.ts | 15 + config/vite/index.ts | 5 +- config/vite/types/BuildOptions.ts | 22 +- config/vite/types/BuildPaths.ts | 14 +- config/vite/types/ResolveOptions.ts | 6 +- config/vite/types/index.ts | 6 +- config/vite/utils/buildAliases.ts | 23 + config/vite/utils/getAppVersion.ts | 19 + config/vite/utils/getBuildOptions.ts | 32 + config/vite/utils/index.ts | 3 + eslint.config.js | 37 +- package-lock.json | 1683 +++++++++++--------- package.json | 34 +- tsconfig.json | 6 +- vitest.config.ts | 67 +- 40 files changed, 1590 insertions(+), 1040 deletions(-) delete mode 100644 .prettierrc.json delete mode 100644 config/eslint/base.ts create mode 100644 config/eslint/common-ignores.ts delete mode 100644 config/eslint/custom-rules.ts create mode 100644 config/eslint/env-appsscript.ts delete mode 100644 config/eslint/globals.ts delete mode 100644 config/eslint/ignores.ts create mode 100644 config/eslint/index.ts delete mode 100644 config/eslint/json.ts create mode 100644 config/eslint/lang-javascript.ts create mode 100644 config/eslint/lang-json.ts create mode 100644 config/eslint/lang-markdown.ts create mode 100644 config/eslint/lang-typescript.ts delete mode 100644 config/eslint/markdown.ts create mode 100644 config/eslint/overrides-tests.ts delete mode 100644 config/eslint/prettier.ts create mode 100644 config/eslint/rules-prettier.ts create mode 100644 config/typescript/tsconfig.appsscript.json create mode 100644 config/typescript/tsconfig.base.json delete mode 100644 config/typescript/tsconfig.json delete mode 100644 config/vite/buildAliases.ts delete mode 100644 config/vite/buildResolversConfig.ts create mode 100644 config/vite/factories/index.ts create mode 100644 config/vite/factories/resolveConfig.ts create mode 100644 config/vite/factories/testConfig.ts create mode 100644 config/vite/utils/buildAliases.ts create mode 100644 config/vite/utils/getAppVersion.ts create mode 100644 config/vite/utils/getBuildOptions.ts create mode 100644 config/vite/utils/index.ts 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/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..5646aca --- /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..4b7e8bd --- /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..ecde0bf --- /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..df6a6e3 --- /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..0c78527 --- /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..6a13643 --- /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..00d0eb3 --- /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-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/typescript/tsconfig.appsscript.json b/config/typescript/tsconfig.appsscript.json new file mode 100644 index 0000000..b267c41 --- /dev/null +++ b/config/typescript/tsconfig.appsscript.json @@ -0,0 +1,96 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + /** + * 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, + + /** + * Base directory to resolve non-relative module names. + * @see https://www.typescriptlang.org/tsconfig#baseUrl + */ + "baseUrl": "../../src", + + /** + * Enable project compilation. + * @see https://www.typescriptlang.org/tsconfig#composite + */ + "composite": false, + + /** + * 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, + + /** + * 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"], + + /** + * 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 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", "../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"], + + /** + * Emit ECMAScript-standard-compliant class fields. + * @see https://www.typescriptlang.org/tsconfig#useDefineForClassFields + */ + "useDefineForClassFields": false + }, + + /** + * Specifies a list of glob patterns that match files to be included in compilation. + * @see https://www.typescriptlang.org/tsconfig#include + */ + "include": ["../../src/**/*", "../../test/**/*", "../../config/**/*"], + + /** + * Specifies a list of files to be excluded from compilation. + * @see https://www.typescriptlang.org/tsconfig#exclude + */ + "exclude": ["../../node_modules", "../../dist"] +} diff --git a/config/typescript/tsconfig.base.json b/config/typescript/tsconfig.base.json new file mode 100644 index 0000000..e1e8373 --- /dev/null +++ b/config/typescript/tsconfig.base.json @@ -0,0 +1,95 @@ +{ + "compilerOptions": { + /** + * Allow importing files with a TypeScript-specific extension (.ts, .tsx, etc.). + * @see https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions + */ + "allowImportingTsExtensions": true, + + /** + * Emit additional JavaScript to ease support for importing CommonJS modules. + * @see https://www.typescriptlang.org/tsconfig#esModuleInterop + */ + "esModuleInterop": 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, + + /** + * 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", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} 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..f0be3f6 100644 --- a/config/typescript/tsconfig.node.json +++ b/config/typescript/tsconfig.node.json @@ -1,29 +1,46 @@ { + "extends": "./tsconfig.base.json", "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"] }, + + /** + * Specifies a list of glob patterns that match files to be included in compilation. + * @see https://www.typescriptlang.org/tsconfig#include + */ "include": ["../../config/**/*", "../../vite.config.ts"], + + /** + * Specifies a list of files to be excluded from compilation. + * @see https://www.typescriptlang.org/tsconfig#exclude + */ "exclude": ["../../node_modules", "../../dist"] } 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..598a71b 100644 --- a/config/vite/types/BuildOptions.ts +++ b/config/vite/types/BuildOptions.ts @@ -1,19 +1,29 @@ 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..b92b151 100644 --- a/config/vite/types/BuildPaths.ts +++ b/config/vite/types/BuildPaths.ts @@ -1,30 +1,30 @@ /** - * 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..1f27e36 100644 --- a/config/vite/types/ResolveOptions.ts +++ b/config/vite/types/ResolveOptions.ts @@ -1,12 +1,12 @@ 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..5b9a21b --- /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 ab8a6c2..b625b06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,26 +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-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" ], @@ -57,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" ], @@ -74,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" ], @@ -91,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" ], @@ -108,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" ], @@ -125,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" ], @@ -142,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" ], @@ -159,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" ], @@ -176,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" ], @@ -193,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" ], @@ -210,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" ], @@ -227,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" ], @@ -244,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" ], @@ -261,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" ], @@ -278,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" ], @@ -295,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" ], @@ -312,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" ], @@ -329,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" ], @@ -346,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" ], @@ -363,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" ], @@ -380,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" ], @@ -397,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" ], @@ -414,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" ], @@ -431,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" ], @@ -448,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" ], @@ -465,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" ], @@ -482,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": { @@ -514,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": { @@ -539,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==", - "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==", + "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": { - "@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": { @@ -602,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": { @@ -615,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": { @@ -631,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", @@ -654,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", @@ -692,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": { @@ -758,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": { @@ -781,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -841,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -855,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -869,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -883,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -897,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -911,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -925,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -939,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -953,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -967,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -981,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -995,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -1009,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -1023,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -1037,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -1051,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -1064,10 +1137,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -1079,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -1093,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -1107,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -1121,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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -1134,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" }, @@ -1177,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" }, @@ -1208,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" } @@ -1226,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" @@ -1250,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" } @@ -1266,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" @@ -1292,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" @@ -1314,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" @@ -1332,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": { @@ -1349,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" @@ -1374,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": { @@ -1388,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" @@ -1443,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" @@ -1467,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": { @@ -1484,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": { @@ -1503,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" @@ -1530,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": { @@ -1543,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": { @@ -1557,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": { @@ -1572,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": { @@ -1582,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": { @@ -1601,7 +1728,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1671,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", @@ -1688,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", @@ -1706,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", @@ -1741,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": { @@ -1798,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", @@ -1891,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", @@ -1904,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": { @@ -1946,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", @@ -2022,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": { @@ -2065,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", @@ -2111,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": { @@ -2183,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", @@ -2227,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", @@ -2251,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": { @@ -2372,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", @@ -2389,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", @@ -2465,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", @@ -2482,6 +2618,52 @@ "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.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2495,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", @@ -2584,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", @@ -2840,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", @@ -3458,20 +3668,6 @@ ], "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", @@ -3518,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", @@ -3581,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", @@ -3616,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" @@ -3668,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": { @@ -3693,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": { @@ -3740,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.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -3768,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.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "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", @@ -3870,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", @@ -3878,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" }, @@ -3918,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", @@ -3941,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", @@ -3983,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": { @@ -4037,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" @@ -4137,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", @@ -4212,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", @@ -4283,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": "*" }, @@ -4296,7 +4444,7 @@ "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { @@ -4322,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..27e87bf 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,18 @@ "description": "Boot Framework for Google Apps Script™ projects.", "main": "src/index.ts", "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/*" }, "repository": { "type": "git", @@ -51,17 +56,18 @@ "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-eslint": "^8.53.0", + "vitest": "^4.0.17" }, "dependencies": { "apps-script-utils": "github:MaksymStoianov/apps-script-utils#main", diff --git a/tsconfig.json b/tsconfig.json index bf84608..c783f04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,13 @@ // https://typescriptlang.org/docs/handbook/tsconfig-json.html { + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, "files": [], "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) + }; +}); From 484e5ebf8cee74384b91da85c46ddeb90a41fe0f Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:33:40 +0100 Subject: [PATCH 04/17] feat: restructure domain models and exceptions --- src/domain/constants/appsscript.ts | 3 ++ src/domain/constants/http.ts | 3 ++ src/domain/constants/index.ts | 3 ++ .../constants/metadata.ts} | 9 +--- src/domain/entities/RouteExecutionContext.ts | 14 ++++++ src/domain/entities/index.ts | 4 +- src/domain/enums/AppsScriptEventType.ts | 49 +++++++++++++++++++ src/domain/enums/HeaderAcceptMimeType.ts | 40 +++++++++++++++ src/{types => domain/enums}/HttpStatus.ts | 0 src/{types => domain/enums}/ParamSource.ts | 4 +- src/{types => domain/enums}/RequestMethod.ts | 0 src/domain/enums/index.ts | 9 ++-- src/domain/index.ts | 7 +-- .../types/ApplicationConfig.ts} | 2 +- src/domain/types/ClassProvider.ts | 7 +++ src/{ => domain}/types/ErrorResponse.ts | 0 src/domain/types/ExistingProvider.ts | 6 +++ src/domain/types/FactoryProvider.ts | 7 +++ src/{ => domain}/types/HttpHeaders.ts | 0 src/{ => domain}/types/HttpRequest.ts | 2 +- src/{ => domain}/types/HttpResponse.ts | 2 +- src/domain/types/InjectTokenDefinition.ts | 8 +++ src/domain/types/InjectionToken.ts | 6 +++ src/domain/types/Newable.ts | 1 + src/domain/types/ParamDefinition.ts | 7 +++ src/domain/types/ParsedUrl.ts | 4 +- src/domain/types/ParsedUrlQuery.ts | 3 ++ src/domain/types/Provider.ts | 12 +++++ src/{ => domain}/types/RouteMetadata.ts | 6 +-- src/domain/types/ValueProvider.ts | 6 +++ src/domain/types/index.ts | 22 +++++++-- src/exceptions/AppException.ts | 15 ++++++ src/exceptions/HttpException.ts | 12 +++++ src/exceptions/README.md | 7 +++ src/exceptions/index.ts | 2 + src/types/AppsScriptEventType.ts | 8 --- src/types/HeaderAcceptMimeType.ts | 7 --- src/types/HttpException.ts | 4 -- src/types/InjectTokenDefinition.ts | 11 ----- src/types/Newable.ts | 2 - src/types/ParamDefinition.ts | 7 --- src/types/ParsedUrl.ts | 8 --- src/types/ParsedUrlQuery.ts | 3 -- src/types/Provider.ts | 3 -- src/types/google-apps-script.d.ts | 12 ----- src/types/index.ts | 18 ------- 46 files changed, 248 insertions(+), 117 deletions(-) create mode 100644 src/domain/constants/appsscript.ts create mode 100644 src/domain/constants/http.ts rename src/{config/constants.ts => domain/constants/metadata.ts} (68%) create mode 100644 src/domain/enums/AppsScriptEventType.ts create mode 100644 src/domain/enums/HeaderAcceptMimeType.ts rename src/{types => domain/enums}/HttpStatus.ts (100%) rename src/{types => domain/enums}/ParamSource.ts (100%) rename src/{types => domain/enums}/RequestMethod.ts (100%) rename src/{types/AppConfig.ts => domain/types/ApplicationConfig.ts} (78%) create mode 100644 src/domain/types/ClassProvider.ts rename src/{ => domain}/types/ErrorResponse.ts (100%) create mode 100644 src/domain/types/ExistingProvider.ts create mode 100644 src/domain/types/FactoryProvider.ts rename src/{ => domain}/types/HttpHeaders.ts (100%) rename src/{ => domain}/types/HttpRequest.ts (80%) rename src/{ => domain}/types/HttpResponse.ts (80%) create mode 100644 src/domain/types/InjectionToken.ts create mode 100644 src/domain/types/Newable.ts create mode 100644 src/domain/types/ParamDefinition.ts create mode 100644 src/domain/types/Provider.ts rename src/{ => domain}/types/RouteMetadata.ts (77%) create mode 100644 src/domain/types/ValueProvider.ts create mode 100644 src/exceptions/AppException.ts create mode 100644 src/exceptions/HttpException.ts create mode 100644 src/exceptions/README.md delete mode 100644 src/types/AppsScriptEventType.ts delete mode 100644 src/types/HeaderAcceptMimeType.ts delete mode 100644 src/types/HttpException.ts delete mode 100644 src/types/InjectTokenDefinition.ts delete mode 100644 src/types/Newable.ts delete mode 100644 src/types/ParamDefinition.ts delete mode 100644 src/types/ParsedUrl.ts delete mode 100644 src/types/ParsedUrlQuery.ts delete mode 100644 src/types/Provider.ts delete mode 100644 src/types/google-apps-script.d.ts delete mode 100644 src/types/index.ts 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 index e69de29..f4596f7 100644 --- a/src/domain/constants/index.ts +++ 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 index e69de29..b186f30 100644 --- a/src/domain/entities/RouteExecutionContext.ts +++ 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 index e42be83..210a9d0 100644 --- a/src/domain/entities/index.ts +++ b/src/domain/entities/index.ts @@ -1,3 +1 @@ -export * from "./constants"; -export * from "./enums"; -export * from "./types"; +export * from "./RouteExecutionContext"; diff --git a/src/domain/enums/AppsScriptEventType.ts b/src/domain/enums/AppsScriptEventType.ts new file mode 100644 index 0000000..30103e1 --- /dev/null +++ b/src/domain/enums/AppsScriptEventType.ts @@ -0,0 +1,49 @@ +/** + * Перечисление типов событий 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..3b8eac2 --- /dev/null +++ b/src/domain/enums/HeaderAcceptMimeType.ts @@ -0,0 +1,40 @@ +/** + * Перечисление 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 index 51b1446..f3ffb85 100644 --- a/src/domain/enums/index.ts +++ b/src/domain/enums/index.ts @@ -1,4 +1,5 @@ -export * from "./constants"; -export * from "./entities"; -export * from "./enums"; -export * from "./types"; +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 index 5da7f59..51b1446 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -1,3 +1,4 @@ -export * from "./events.constants"; -export * from "./http.constants"; -export * from "./metadata.constants"; +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 80% rename from src/types/HttpRequest.ts rename to src/domain/types/HttpRequest.ts index 924e7fc..c5c0409 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 80% rename from src/types/HttpResponse.ts rename to src/domain/types/HttpResponse.ts index 276c6c6..824c470 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 index e69de29..5d1b041 100644 --- a/src/domain/types/InjectTokenDefinition.ts +++ 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..d7257e6 --- /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 index fdf0b98..84d3ee2 100644 --- a/src/domain/types/ParsedUrl.ts +++ b/src/domain/types/ParsedUrl.ts @@ -1,6 +1,4 @@ -export interface ParsedUrlQuery { - [key: string]: string | string[] | undefined; -} +import { ParsedUrlQuery } from "./ParsedUrlQuery"; export interface ParsedUrl { pathname: string; diff --git a/src/domain/types/ParsedUrlQuery.ts b/src/domain/types/ParsedUrlQuery.ts index e69de29..010eb6e 100644 --- a/src/domain/types/ParsedUrlQuery.ts +++ b/src/domain/types/ParsedUrlQuery.ts @@ -0,0 +1,3 @@ +export interface ParsedUrlQuery { + [key: string]: string | string[] | undefined; +} 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 77% rename from src/types/RouteMetadata.ts rename to src/domain/types/RouteMetadata.ts index e7e1d79..3475a35 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 index f3ffb85..73782f3 100644 --- a/src/domain/types/index.ts +++ b/src/domain/types/index.ts @@ -1,5 +1,17 @@ -export * from "./AppsScriptEventType"; -export * from "./HeaderAcceptMimeType"; -export * from "./HttpStatus"; -export * from "./ParamSource"; -export * from "./RequestMethod"; +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..cbd274f --- /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 index e69de29..0ec57e6 100644 --- a/src/exceptions/index.ts +++ b/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from "./AppException"; +export * from "./HttpException"; 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/ParsedUrlQuery.ts b/src/types/ParsedUrlQuery.ts deleted file mode 100644 index 010eb6e..0000000 --- a/src/types/ParsedUrlQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ParsedUrlQuery { - [key: string]: string | string[] | undefined; -} 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"; From 2e8c043acc07d4e6eb0b3430c26a1083c650eb79 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:33:47 +0100 Subject: [PATCH 05/17] feat: restructure repositories and shared utilities --- src/repository/MetadataRepository.ts | 71 ++++++++ src/repository/assignInjectMetadata.ts | 23 +++ src/repository/assignParamMetadata.ts | 5 +- src/repository/createAppsScriptDecorator.ts | 25 +++ src/repository/createHttpDecorator.ts | 25 +++ .../createMethodDecorator.ts | 28 ++- src/repository/createParamDecorator.ts | 31 ++++ src/repository/getInjectionTokens.ts | 22 +++ src/repository/index.ts | 11 +- src/{ => shared}/utils/createRequest.ts | 21 +-- src/{ => shared}/utils/createResponse.ts | 15 +- src/{ => shared}/utils/extractPathParams.ts | 2 +- src/shared/utils/index.ts | 7 + src/{ => shared}/utils/isController.ts | 4 +- src/{ => shared}/utils/isInjectable.ts | 4 +- src/{ => shared}/utils/pathMatch.ts | 2 +- src/{ => shared}/utils/wrapResponse.ts | 5 +- src/utils/RouterExplorer.ts | 53 ------ src/utils/assignInjectMetadata.ts | 23 --- src/utils/assignMetadata.ts | 23 --- src/utils/buildMethodParams.ts | 167 ------------------ src/utils/checkEventFilters.ts | 86 --------- src/utils/createHttpDecorator.ts | 35 ---- src/utils/createParamDecorator.ts | 41 ----- src/utils/getInjectionTokens.ts | 31 ---- src/utils/index.ts | 24 --- src/utils/resolve.ts | 81 --------- 27 files changed, 247 insertions(+), 618 deletions(-) create mode 100644 src/repository/MetadataRepository.ts create mode 100644 src/repository/createAppsScriptDecorator.ts create mode 100644 src/repository/createHttpDecorator.ts rename src/{utils => repository}/createMethodDecorator.ts (63%) create mode 100644 src/repository/createParamDecorator.ts create mode 100644 src/repository/getInjectionTokens.ts rename src/{ => shared}/utils/createRequest.ts (76%) rename src/{ => shared}/utils/createResponse.ts (82%) rename src/{ => shared}/utils/extractPathParams.ts (94%) create mode 100644 src/shared/utils/index.ts rename src/{ => shared}/utils/isController.ts (55%) rename src/{ => shared}/utils/isInjectable.ts (55%) rename src/{ => shared}/utils/pathMatch.ts (94%) rename src/{ => shared}/utils/wrapResponse.ts (90%) delete mode 100644 src/utils/RouterExplorer.ts delete mode 100644 src/utils/assignInjectMetadata.ts delete mode 100644 src/utils/assignMetadata.ts delete mode 100644 src/utils/buildMethodParams.ts delete mode 100644 src/utils/checkEventFilters.ts delete mode 100644 src/utils/createHttpDecorator.ts delete mode 100644 src/utils/createParamDecorator.ts delete mode 100644 src/utils/getInjectionTokens.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/resolve.ts 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/repository/assignInjectMetadata.ts b/src/repository/assignInjectMetadata.ts index e69de29..947dc98 100644 --- a/src/repository/assignInjectMetadata.ts +++ b/src/repository/assignInjectMetadata.ts @@ -0,0 +1,23 @@ +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. + * + * @param existing - The existing injection tokens metadata. + * @param index - The index of the parameter in the function's argument list. + * @param [token] - The injection token for this parameter. + * @returns The updated injection tokens metadata. + */ +export function assignInjectMetadata( + existing: Record, + index: number, + token?: InjectionToken +): Record { + const type = ParamSource.INJECT; + + return { + ...existing, + [ `${type as string}:${index}` ]: { type, token, index } + }; +} diff --git a/src/repository/assignParamMetadata.ts b/src/repository/assignParamMetadata.ts index 7acef37..99d25a3 100644 --- a/src/repository/assignParamMetadata.ts +++ b/src/repository/assignParamMetadata.ts @@ -1,4 +1,5 @@ -import { ParamDefinition, ParamSource } from "../../../domain"; +import { ParamDefinition } from "domain/types"; +import { ParamSource } from "domain/enums"; /** * Updates parameter metadata with the argument's position (index). @@ -17,6 +18,6 @@ export function assignParamMetadata( ): Record { return { ...existing, - [`${type as string}:${index}`]: { type, key, index } + [ `${type as string}:${index}` ]: { type, key, index } }; } diff --git a/src/repository/createAppsScriptDecorator.ts b/src/repository/createAppsScriptDecorator.ts new file mode 100644 index 0000000..a5ad98b --- /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..1dc5946 --- /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..4c8be22 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..454814d --- /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..a88f153 --- /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 index e42be83..2fca9d0 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -1,3 +1,8 @@ -export * from "./constants"; -export * from "./enums"; -export * from "./types"; +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/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..029c371 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 82% rename from src/utils/createResponse.ts rename to src/shared/utils/createResponse.ts index ae5015a..7e372d3 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,7 +22,7 @@ export function createResponse( ): HttpResponse { const resolvedStatus = status ?? - ([RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS].includes( + ([ RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS ].includes( request.method ) ? HttpStatus.OK @@ -35,10 +30,10 @@ export function createResponse( const statusText = ((): string => { const entry = Object.entries(HttpStatus).find( - ([, value]) => value === resolvedStatus + ([ , value ]) => value === resolvedStatus ); - return entry ? entry[0] : "UNKNOWN_STATUS"; + return entry ? entry[ 0 ] : "UNKNOWN_STATUS"; })(); const ok = resolvedStatus >= 200 && resolvedStatus < 300; diff --git a/src/utils/extractPathParams.ts b/src/shared/utils/extractPathParams.ts similarity index 94% rename from src/utils/extractPathParams.ts rename to src/shared/utils/extractPathParams.ts index d96d179..f53b16f 100644 --- a/src/utils/extractPathParams.ts +++ b/src/shared/utils/extractPathParams.ts @@ -17,7 +17,7 @@ export function extractPathParams( tplParts.forEach((part, i) => { if (part.startsWith("{") && part.endsWith("}")) { const paramName = part.slice(1, -1); - params[paramName] = actParts[i]; + params[ paramName ] = actParts[ i ]; } }); 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 55% rename from src/utils/isController.ts rename to src/shared/utils/isController.ts index 6352652..2911115 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 55% rename from src/utils/isInjectable.ts rename to src/shared/utils/isInjectable.ts index 269bc57..0a9df11 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 94% rename from src/utils/pathMatch.ts rename to src/shared/utils/pathMatch.ts index e99b2c1..39ee00f 100644 --- a/src/utils/pathMatch.ts +++ b/src/shared/utils/pathMatch.ts @@ -17,6 +17,6 @@ export function pathMatch(template: string, actual: string): boolean { if (part.startsWith("{") && part.endsWith("}")) { return true; } - return part === actParts[i]; + return part === actParts[ i ]; }); } diff --git a/src/utils/wrapResponse.ts b/src/shared/utils/wrapResponse.ts similarity index 90% rename from src/utils/wrapResponse.ts rename to src/shared/utils/wrapResponse.ts index aa39969..6445b2c 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`). @@ -20,7 +21,7 @@ export function wrapResponse( (request.headers?.Accept as HeaderAcceptMimeType) || HeaderAcceptMimeType.HTML; - response.headers["Content-Type"] = mimeType; + response.headers[ "Content-Type" ] = mimeType; const isApi = request.url.pathname?.startsWith("/api/") || false; const result = JSON.stringify(isApi ? response : response.body); diff --git a/src/utils/RouterExplorer.ts b/src/utils/RouterExplorer.ts deleted file mode 100644 index 3ca3beb..0000000 --- a/src/utils/RouterExplorer.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { normalize } from "apps-script-utils"; -import { - CONTROLLER_OPTIONS_METADATA, - CONTROLLER_TYPE_METADATA, - METHOD_METADATA, - PATH_METADATA -} from "../config/constants"; -import { Newable, RouteMetadata } from "../types"; - -export class RouterExplorer { - static explore(controllers: Map): RouteMetadata[] { - const routes: RouteMetadata[] = []; - - for (const controller of controllers.keys()) { - const controllerType: string | null = - Reflect.getMetadata(CONTROLLER_TYPE_METADATA, controller) || null; - - const isHttpController = controllerType === "http"; - - if (!isHttpController) continue; - - 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; - - const methodHandler = prototype[propertyName]; - const routePath = Reflect.getMetadata(PATH_METADATA, methodHandler); - const requestMethod = Reflect.getMetadata( - METHOD_METADATA, - methodHandler - ); - - if (routePath && requestMethod) { - routes.push({ - controller, - handler: propertyName, - method: requestMethod, - path: decodeURI(normalize(`/${basePath}/${routePath}`)) - }); - } - } - } - - return routes; - } -} diff --git a/src/utils/assignInjectMetadata.ts b/src/utils/assignInjectMetadata.ts deleted file mode 100644 index 4a0d6f6..0000000 --- a/src/utils/assignInjectMetadata.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { InjectTokenDefinition, ParamSource } from "../types"; - -/** - * Updates or adds metadata for the injection tokens of a specific function parameter (argument) based on its index and token. - * - * @param existing - The existing injection tokens metadata. - * @param index - The index of the parameter in the function's argument list. - * @param [token] - The injection token for this parameter. - * @returns The updated injection tokens metadata. - */ -export function assignInjectMetadata( - existing: Record, - index: number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - token?: any -): Record { - const type = ParamSource.INJECT; - - return { - ...existing, - [`${type as string}:${index}`]: { type, token, index } - }; -} diff --git a/src/utils/assignMetadata.ts b/src/utils/assignMetadata.ts deleted file mode 100644 index 3993354..0000000 --- a/src/utils/assignMetadata.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ParamDefinition, ParamSource } from "../types"; - -/** - * 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. - * @returns The updated parameter metadata. - */ -export function assignMetadata( - existing: Record, - index: number, - type: ParamSource, - key?: string -): Record { - return { - ...existing, - [`${type as string}:${index}`]: { type, key, index } - }; -} 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 deleted file mode 100644 index 398d265..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -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"; - -// decorators/class - -// 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"; 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; -} From 2ebad08d59d065177073af68b284dc710e27d4d6 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:33:55 +0100 Subject: [PATCH 06/17] feat: restructure controllers and services --- src/App.ts | 489 ------------------ src/controller/BootApplication.ts | 98 ++++ src/controller/BootApplicationFactory.ts | 14 + src/controller/decorators/Controller.ts | 23 + src/controller/decorators/Entity.ts | 14 + src/controller/decorators/HttpController.ts | 11 + src/controller/decorators/Inject.ts | 28 + src/controller/decorators/Injectable.ts | 12 + src/controller/decorators/Repository.ts | 10 + src/controller/decorators/RestController.ts | 3 + src/controller/decorators/Service.ts | 10 + .../decorators/appsscript/OnChange.ts | 4 + .../decorators/appsscript/OnEdit.ts | 4 + .../decorators/appsscript/OnFormSubmit.ts | 4 + .../decorators/appsscript/OnInstall.ts | 4 + .../decorators/appsscript/OnOpen.ts | 4 + src/controller/decorators/appsscript/index.ts | 5 + src/controller/decorators/index.ts | 12 + src/controller/decorators/params/Body.ts | 10 + src/controller/decorators/params/Event.ts | 10 + src/controller/decorators/params/Headers.ts | 10 + src/controller/decorators/params/Param.ts | 10 + .../decorators/params/PathVariable.ts | 3 + src/controller/decorators/params/Query.ts | 10 + src/controller/decorators/params/Request.ts | 10 + .../decorators/params/RequestBody.ts | 3 + .../decorators/params/RequestParam.ts | 3 + src/controller/decorators/params/Response.ts | 10 + .../decorators/params/index.ts | 8 +- src/controller/decorators/routing/Delete.ts | 10 + .../decorators/routing/DeleteMapping.ts | 10 + src/controller/decorators/routing/Get.ts | 10 + .../decorators/routing/GetMapping.ts | 10 + src/controller/decorators/routing/Head.ts | 10 + .../decorators/routing/HeadMapping.ts | 10 + src/controller/decorators/routing/Options.ts | 10 + .../decorators/routing/OptionsMapping.ts | 10 + src/controller/decorators/routing/Patch.ts | 10 + .../decorators/routing/PatchMapping.ts | 10 + src/controller/decorators/routing/Post.ts | 10 + .../decorators/routing/PostMapping.ts | 10 + src/controller/decorators/routing/Put.ts | 10 + .../decorators/routing/PutMapping.ts | 10 + src/controller/decorators/routing/index.ts | 14 + .../decorators/security}/index.ts | 0 .../decorators/validation/index.ts | 0 src/controller/index.ts | 3 + src/controllers/decorators/RestController.ts | 3 - src/controllers/decorators/index.ts | 55 -- .../decorators/params/PathVariable.ts | 3 - .../decorators/params/RequestBody.ts | 3 - .../decorators/params/RequestParam.ts | 3 - src/controllers/decorators/routing/Delete.ts | 0 .../decorators/routing/DeleteMapping.ts | 0 .../decorators/routing/GetMapping.ts | 0 src/controllers/decorators/routing/Head.ts | 0 .../decorators/routing/HeadMapping.ts | 0 src/controllers/decorators/routing/Options.ts | 0 src/controllers/decorators/routing/Patch.ts | 0 .../decorators/routing/PatchMapping.ts | 0 src/controllers/decorators/routing/Post.ts | 0 .../decorators/routing/PostMapping.ts | 0 src/controllers/decorators/routing/Put.ts | 0 .../decorators/routing/PutMapping.ts | 0 src/controllers/decorators/security/index.ts | 7 - src/controllers/index.ts | 2 - src/decorators/class.ts | 230 -------- src/decorators/index.ts | 3 - src/decorators/method.ts | 336 ------------ src/decorators/param.ts | 207 -------- src/index.ts | 10 +- src/service/EventDispatcher.ts | 162 ++++++ src/service/PathMatcher.ts | 46 ++ src/service/RequestFactory.ts | 91 ++++ src/service/Resolver.ts | 93 ++++ src/service/ResponseBuilder.ts | 82 +++ src/service/Router.ts | 178 +++++++ src/service/RouterExplorer.ts | 51 ++ src/service/index.ts | 7 + src/services/index.ts | 52 -- 80 files changed, 1215 insertions(+), 1402 deletions(-) delete mode 100644 src/App.ts create mode 100644 src/controller/BootApplication.ts create mode 100644 src/controller/BootApplicationFactory.ts create mode 100644 src/controller/decorators/Controller.ts create mode 100644 src/controller/decorators/Entity.ts create mode 100644 src/controller/decorators/HttpController.ts create mode 100644 src/controller/decorators/Inject.ts create mode 100644 src/controller/decorators/Injectable.ts create mode 100644 src/controller/decorators/Repository.ts create mode 100644 src/controller/decorators/RestController.ts create mode 100644 src/controller/decorators/Service.ts create mode 100644 src/controller/decorators/appsscript/OnChange.ts create mode 100644 src/controller/decorators/appsscript/OnEdit.ts create mode 100644 src/controller/decorators/appsscript/OnFormSubmit.ts create mode 100644 src/controller/decorators/appsscript/OnInstall.ts create mode 100644 src/controller/decorators/appsscript/OnOpen.ts create mode 100644 src/controller/decorators/appsscript/index.ts create mode 100644 src/controller/decorators/index.ts create mode 100644 src/controller/decorators/params/Body.ts create mode 100644 src/controller/decorators/params/Event.ts create mode 100644 src/controller/decorators/params/Headers.ts create mode 100644 src/controller/decorators/params/Param.ts create mode 100644 src/controller/decorators/params/PathVariable.ts create mode 100644 src/controller/decorators/params/Query.ts create mode 100644 src/controller/decorators/params/Request.ts create mode 100644 src/controller/decorators/params/RequestBody.ts create mode 100644 src/controller/decorators/params/RequestParam.ts create mode 100644 src/controller/decorators/params/Response.ts rename src/{controllers => controller}/decorators/params/index.ts (61%) create mode 100644 src/controller/decorators/routing/Delete.ts create mode 100644 src/controller/decorators/routing/DeleteMapping.ts create mode 100644 src/controller/decorators/routing/Get.ts create mode 100644 src/controller/decorators/routing/GetMapping.ts create mode 100644 src/controller/decorators/routing/Head.ts create mode 100644 src/controller/decorators/routing/HeadMapping.ts create mode 100644 src/controller/decorators/routing/Options.ts create mode 100644 src/controller/decorators/routing/OptionsMapping.ts create mode 100644 src/controller/decorators/routing/Patch.ts create mode 100644 src/controller/decorators/routing/PatchMapping.ts create mode 100644 src/controller/decorators/routing/Post.ts create mode 100644 src/controller/decorators/routing/PostMapping.ts create mode 100644 src/controller/decorators/routing/Put.ts create mode 100644 src/controller/decorators/routing/PutMapping.ts create mode 100644 src/controller/decorators/routing/index.ts rename src/{controllers/decorators/routing => controller/decorators/security}/index.ts (100%) rename src/{controllers => controller}/decorators/validation/index.ts (100%) create mode 100644 src/controller/index.ts delete mode 100644 src/controllers/decorators/RestController.ts delete mode 100644 src/controllers/decorators/index.ts delete mode 100644 src/controllers/decorators/params/PathVariable.ts delete mode 100644 src/controllers/decorators/params/RequestBody.ts delete mode 100644 src/controllers/decorators/params/RequestParam.ts delete mode 100644 src/controllers/decorators/routing/Delete.ts delete mode 100644 src/controllers/decorators/routing/DeleteMapping.ts delete mode 100644 src/controllers/decorators/routing/GetMapping.ts delete mode 100644 src/controllers/decorators/routing/Head.ts delete mode 100644 src/controllers/decorators/routing/HeadMapping.ts delete mode 100644 src/controllers/decorators/routing/Options.ts delete mode 100644 src/controllers/decorators/routing/Patch.ts delete mode 100644 src/controllers/decorators/routing/PatchMapping.ts delete mode 100644 src/controllers/decorators/routing/Post.ts delete mode 100644 src/controllers/decorators/routing/PostMapping.ts delete mode 100644 src/controllers/decorators/routing/Put.ts delete mode 100644 src/controllers/decorators/routing/PutMapping.ts delete mode 100644 src/controllers/decorators/security/index.ts delete mode 100644 src/controllers/index.ts delete mode 100644 src/decorators/class.ts delete mode 100644 src/decorators/index.ts delete mode 100644 src/decorators/method.ts delete mode 100644 src/decorators/param.ts create mode 100644 src/service/EventDispatcher.ts create mode 100644 src/service/PathMatcher.ts create mode 100644 src/service/RequestFactory.ts create mode 100644 src/service/Resolver.ts create mode 100644 src/service/ResponseBuilder.ts create mode 100644 src/service/Router.ts create mode 100644 src/service/RouterExplorer.ts create mode 100644 src/service/index.ts delete mode 100644 src/services/index.ts 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..1269314 --- /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 + // 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..721f915 --- /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..a860c5f --- /dev/null +++ b/src/controller/decorators/Controller.ts @@ -0,0 +1,23 @@ +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..f4f287f --- /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..eb94153 --- /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..bc71abc --- /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..0059a5d --- /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..dea75d4 --- /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..ad796a4 --- /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..8c9f091 --- /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..0851315 --- /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..46fba92 --- /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..6696f86 --- /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..6d1abfc --- /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..4b771f5 --- /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..69acce7 --- /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..dd2348d --- /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..2681348 --- /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..c6cb054 --- /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..bdcd160 --- /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..b38a01a --- /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..4f57188 --- /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..6d817b4 --- /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..809a4c6 --- /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..8ac3e6a --- /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/controllers/decorators/params/index.ts b/src/controller/decorators/params/index.ts similarity index 61% rename from src/controllers/decorators/params/index.ts rename to src/controller/decorators/params/index.ts index 16cfdf0..ab0c483 100644 --- a/src/controllers/decorators/params/index.ts +++ b/src/controller/decorators/params/index.ts @@ -1,8 +1,10 @@ export * from "./Body"; -export * from "./RequestBody"; - +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..dcdb963 --- /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..da9cf30 --- /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..a245a4c --- /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..403a406 --- /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..b43c5f8 --- /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..123cef3 --- /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..d563d07 --- /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..701c425 --- /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..98d16bf --- /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..b4ba42f --- /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..b066bbb --- /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..9d4332a --- /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..0cba2d1 --- /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..40cc75c --- /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/controllers/decorators/routing/index.ts b/src/controller/decorators/security/index.ts similarity index 100% rename from src/controllers/decorators/routing/index.ts rename to src/controller/decorators/security/index.ts diff --git a/src/controllers/decorators/validation/index.ts b/src/controller/decorators/validation/index.ts similarity index 100% rename from src/controllers/decorators/validation/index.ts rename to src/controller/decorators/validation/index.ts 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/controllers/decorators/RestController.ts b/src/controllers/decorators/RestController.ts deleted file mode 100644 index 18b2a6a..0000000 --- a/src/controllers/decorators/RestController.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { HttpController } from "./HttpController"; - -export const RestController = HttpController; diff --git a/src/controllers/decorators/index.ts b/src/controllers/decorators/index.ts deleted file mode 100644 index 4d35e01..0000000 --- a/src/controllers/decorators/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Core -export { BootApplication } from "./controllers/main/application"; -export { ApplicationFactory } from "./controllers/main/application-factory"; - -// Services -export { Resolver } from "./services/resolver"; -export { Inject } from "./services/inject.decorator"; -export { EventDispatcher } from "./services/event-dispatcher"; -export { Service } from "./services/service.decorator"; - -// Decorators -export { Injectable } from "./controllers/decorators/Injectable"; -export { - HttpController, - RestController -} from "./controllers/decorators/routing/http-controller.decorator"; -export { Get } from "./controllers/decorators/routing/get.decorator"; -export { Controller } from "./controllers/decorators/routing/controller.decorator"; -export { Repository } from "./controllers/decorators/routing/repository.decorator"; - -export { - Post, - Put, - Delete, - Patch, - Head, - Options, - GetMapping, - PostMapping, - PutMapping, - DeleteMapping, - PatchMapping, - HeadMapping, - OptionsMapping -} from "./controllers/decorators/routing/methods.decorator"; - -export { Param, PathVariable } from "./controllers/decorators/params/Param"; -export { Query, RequestParam } from "./controllers/decorators/params/Query"; -export { Body, RequestBody } from "./controllers/decorators/params/Body"; - -// Domain -export { Entity } from "./controllers/decorators/Entity"; -export { Newable } from "./domain/types/newable.type"; -export { Provider } from "./domain/types/provider.type"; -export { ApplicationConfig } from "./domain/types/application-config.interface"; -export { RequestMethod } from "./domain/enums/request-method.enum"; -export { HttpStatus } from "./domain/enums/http-status.enum"; -export { AppsScriptEventType } from "./domain/enums/apps-script-event-type.enum"; - -// Repository -export { MetadataRepository } from "./repository/MetadataRepository"; - -// Exceptions -export { AppException } from "./exceptions/app.exception"; -export { HttpException } from "./exceptions/http.exception"; diff --git a/src/controllers/decorators/params/PathVariable.ts b/src/controllers/decorators/params/PathVariable.ts deleted file mode 100644 index 06b4355..0000000 --- a/src/controllers/decorators/params/PathVariable.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Param } from "./Param"; - -export const PathVariable = Param; diff --git a/src/controllers/decorators/params/RequestBody.ts b/src/controllers/decorators/params/RequestBody.ts deleted file mode 100644 index 48e650f..0000000 --- a/src/controllers/decorators/params/RequestBody.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Body } from "./Body"; - -export const RequestBody = Body; diff --git a/src/controllers/decorators/params/RequestParam.ts b/src/controllers/decorators/params/RequestParam.ts deleted file mode 100644 index b72b11a..0000000 --- a/src/controllers/decorators/params/RequestParam.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Query } from "./Query"; - -export const RequestParam = Query; diff --git a/src/controllers/decorators/routing/Delete.ts b/src/controllers/decorators/routing/Delete.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/DeleteMapping.ts b/src/controllers/decorators/routing/DeleteMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/GetMapping.ts b/src/controllers/decorators/routing/GetMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/Head.ts b/src/controllers/decorators/routing/Head.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/HeadMapping.ts b/src/controllers/decorators/routing/HeadMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/Options.ts b/src/controllers/decorators/routing/Options.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/Patch.ts b/src/controllers/decorators/routing/Patch.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/PatchMapping.ts b/src/controllers/decorators/routing/PatchMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/Post.ts b/src/controllers/decorators/routing/Post.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/PostMapping.ts b/src/controllers/decorators/routing/PostMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/Put.ts b/src/controllers/decorators/routing/Put.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/routing/PutMapping.ts b/src/controllers/decorators/routing/PutMapping.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/decorators/security/index.ts b/src/controllers/decorators/security/index.ts deleted file mode 100644 index 4f29d8a..0000000 --- a/src/controllers/decorators/security/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./params"; -export * from "./routing"; -export * from "./security"; -export * from "./validation"; - -export * from "./Entity"; -export * from "./Injectable"; diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index 0ec57e6..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./AppException"; -export * from "./HttpException"; 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/index.ts b/src/index.ts index 5f23af7..26cbed7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,6 @@ -import "reflect-metadata"; -import { App } from "./App"; +import { BootApplication, BootApplicationFactory } from "controller"; -export * from "./types"; -export * from "./decorators"; +export * from "controller"; -export { App }; -export const createApp = App.create; +export { BootApplication as App }; +export const createApp = BootApplicationFactory.create; diff --git a/src/service/EventDispatcher.ts b/src/service/EventDispatcher.ts new file mode 100644 index 0000000..1d22b20 --- /dev/null +++ b/src/service/EventDispatcher.ts @@ -0,0 +1,162 @@ +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.buildArgs(instance as object, propertyName, event); + + const handler = (instance as Record)[ propertyName ] as ( + ...args: unknown[] + ) => unknown; + await handler.apply(instance, 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; + } + + private buildArgs(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; + } +} diff --git a/src/service/PathMatcher.ts b/src/service/PathMatcher.ts new file mode 100644 index 0000000..c621f81 --- /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..2d3ba52 --- /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..b7b98af --- /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..ad3b4b4 --- /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..879ba3e --- /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/service/RouterExplorer.ts b/src/service/RouterExplorer.ts new file mode 100644 index 0000000..85c8e83 --- /dev/null +++ b/src/service/RouterExplorer.ts @@ -0,0 +1,51 @@ +import { normalize } from "apps-script-utils"; +import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, METHOD_METADATA, PATH_METADATA } from "domain/constants"; +import { Newable, RouteMetadata } from "domain/types"; + +export class RouterExplorer { + public explore(controllers: Map): RouteMetadata[] { + const routes: RouteMetadata[] = []; + + for (const controller of controllers.keys()) { + const controllerType: string | null = + Reflect.getMetadata(CONTROLLER_TYPE_METADATA, controller) || null; + + const isHttpController = controllerType === "http"; + + if (!isHttpController) { + continue; + } + + 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; + } + + const methodHandler = prototype[ propertyName ]; + + const routePath = Reflect.getMetadata(PATH_METADATA, methodHandler); + + const requestMethod = Reflect.getMetadata(METHOD_METADATA, methodHandler); + + if (routePath && requestMethod) { + routes.push({ + controller, + handler: propertyName, + method: requestMethod, + path: decodeURI(normalize(`/${basePath}/${routePath}`)) + }); + } + } + } + + return routes; + } +} 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/services/index.ts b/src/services/index.ts deleted file mode 100644 index 64ef6c4..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Core -export { BootApplication } from "./controllers/BootApplication"; -export { BootApplicationFactory } from "./controllers/BootApplicationFactory"; - -// Services -export { Resolver } from "./services/Resolver"; -export { Inject } from "./controllers/decorators/Inject"; -export { EventDispatcher } from "./services/EventDispatcher"; -export { Service } from "./controllers/decorators/Service"; - -// Decorators -export { Injectable } from "./controllers/decorators/Injectable"; -export { HttpController, RestController } from "./controllers/decorators/HttpController"; -export { Get } from "./controllers/decorators/routing/Get"; -export { Controller } from "./controllers/decorators/routing/controller.decorator"; -export { Repository } from "./controllers/decorators/Repository"; - -export { - Post, - Put, - Delete, - Patch, - Head, - Options, - GetMapping, - PostMapping, - PutMapping, - DeleteMapping, - PatchMapping, - HeadMapping, - OptionsMapping -} from "./controllers/decorators/routing/OptionsMapping"; - -export { Param, PathVariable } from "./controllers/decorators/params/Param"; -export { Query, RequestParam } from "./controllers/decorators/params/Query"; -export { Body, RequestBody } from "./controllers/decorators/params/Body"; - -// Domain -export { Entity } from "./controllers/decorators/Entity"; -export { Newable } from "./domain/types/newable.type"; -export { Provider } from "./domain/types/provider.type"; -export { ApplicationConfig } from "./domain/types/application-config.interface"; -export { RequestMethod } from "./domain/enums/request-method.enum"; -export { HttpStatus } from "./domain/enums/http-status.enum"; -export { AppsScriptEventType } from "./domain/enums/apps-script-event-type.enum"; - -// Repository -export { MetadataRepository } from "./repository/MetadataRepository"; - -// Exceptions -export { AppException } from "./exceptions/AppException"; -export { HttpException } from "./exceptions/HttpException"; From 97b4e3ea695a5b742da705f7c97e70f6912c17aa Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:34:02 +0100 Subject: [PATCH 07/17] test: update and restructure tests --- test/decorators/class.test.ts | 197 -------- test/decorators/method.test.ts | 194 -------- test/decorators/param.test.ts | 450 ------------------ ...otApplication.boundary.integration.test.ts | 18 + ...otApplication.negative.integration.test.ts | 38 ++ ...otApplication.positive.integration.test.ts | 77 +++ .../BootApplication.positive.unit.test.ts | 58 +++ ...otApplicationFactory.positive.unit.test.ts | 13 + .../Controller.negative.unit.test.ts | 10 + .../Controller.positive.unit.test.ts | 25 + .../decorators/Decorators.extra.unit.test.ts | 50 ++ .../Entity/Entity.boundary.unit.test.ts | 14 + .../Entity/Entity.negative.unit.test.ts | 10 + .../Entity/Entity.positive.unit.test.ts | 13 + .../HttpController.boundary.unit.test.ts | 41 ++ .../HttpController.negative.unit.test.ts | 15 + .../HttpController.positive.unit.test.ts | 59 +++ .../Inject/Inject.boundary.unit.test.ts | 26 + .../Inject/Inject.negative.unit.test.ts | 26 + .../Inject/Inject.positive.unit.test.ts | 47 ++ .../Injectable.boundary.unit.test.ts | 14 + .../Injectable.negative.unit.test.ts | 10 + .../Injectable.positive.unit.test.ts | 13 + .../Repository.boundary.unit.test.ts | 14 + .../Repository.negative.unit.test.ts | 10 + .../Repository.positive.unit.test.ts | 13 + .../RestController.boundary.unit.test.ts | 18 + .../RestController.negative.unit.test.ts | 10 + .../RestController.positive.unit.test.ts | 26 + .../Service/Service.boundary.unit.test.ts | 14 + .../Service/Service.negative.unit.test.ts | 10 + .../Service/Service.positive.unit.test.ts | 13 + .../OnChange/OnChange.boundary.unit.test.ts | 20 + .../OnChange/OnChange.negative.unit.test.ts | 13 + .../OnChange/OnChange.positive.unit.test.ts | 32 ++ .../OnEdit/OnEdit.boundary.unit.test.ts | 20 + .../OnEdit/OnEdit.negative.unit.test.ts | 13 + .../OnEdit/OnEdit.positive.unit.test.ts | 32 ++ .../OnFormSubmit.boundary.unit.test.ts | 20 + .../OnFormSubmit.negative.unit.test.ts | 13 + .../OnFormSubmit.positive.unit.test.ts | 32 ++ .../OnInstall/OnInstall.boundary.unit.test.ts | 20 + .../OnInstall/OnInstall.negative.unit.test.ts | 13 + .../OnInstall/OnInstall.positive.unit.test.ts | 32 ++ .../OnOpen/OnOpen.boundary.unit.test.ts | 20 + .../OnOpen/OnOpen.negative.unit.test.ts | 13 + .../OnOpen/OnOpen.positive.unit.test.ts | 32 ++ .../params/Body/Body.boundary.unit.test.ts | 26 + .../params/Body/Body.negative.unit.test.ts | 22 + .../params/Body/Body.positive.unit.test.ts | 48 ++ .../params/Event/Event.boundary.unit.test.ts | 26 + .../params/Event/Event.negative.unit.test.ts | 22 + .../params/Event/Event.positive.unit.test.ts | 37 ++ .../Headers/Headers.boundary.unit.test.ts | 26 + .../Headers/Headers.negative.unit.test.ts | 22 + .../Headers/Headers.positive.unit.test.ts | 41 ++ .../Integration.positive.unit.test.ts | 57 +++ .../params/Param/Param.boundary.unit.test.ts | 37 ++ .../params/Param/Param.negative.unit.test.ts | 31 ++ .../params/Param/Param.positive.unit.test.ts | 48 ++ .../PathVariable.boundary.unit.test.ts | 39 ++ .../PathVariable.negative.unit.test.ts | 22 + .../PathVariable.positive.unit.test.ts | 37 ++ .../params/Query/Query.boundary.unit.test.ts | 26 + .../params/Query/Query.negative.unit.test.ts | 22 + .../params/Query/Query.positive.unit.test.ts | 48 ++ .../Request/Request.boundary.unit.test.ts | 26 + .../Request/Request.negative.unit.test.ts | 22 + .../Request/Request.positive.unit.test.ts | 37 ++ .../RequestBody.boundary.unit.test.ts | 26 + .../RequestBody.negative.unit.test.ts | 22 + .../RequestBody.positive.unit.test.ts | 37 ++ .../RequestParam.boundary.unit.test.ts | 26 + .../RequestParam.negative.unit.test.ts | 22 + .../RequestParam.positive.unit.test.ts | 37 ++ .../Response/Response.boundary.unit.test.ts | 26 + .../Response/Response.negative.unit.test.ts | 22 + .../Response/Response.positive.unit.test.ts | 37 ++ .../Delete/Delete.boundary.unit.test.ts | 24 + .../Delete/Delete.negative.unit.test.ts | 14 + .../Delete/Delete.positive.unit.test.ts | 58 +++ .../DeleteMapping.boundary.unit.test.ts | 18 + .../DeleteMapping.negative.unit.test.ts | 14 + .../DeleteMapping.positive.unit.test.ts | 28 ++ .../routing/Get/Get.boundary.unit.test.ts | 24 + .../routing/Get/Get.negative.unit.test.ts | 14 + .../routing/Get/Get.positive.unit.test.ts | 58 +++ .../GetMapping.boundary.unit.test.ts | 18 + .../GetMapping.negative.unit.test.ts | 14 + .../GetMapping.positive.unit.test.ts | 28 ++ .../routing/Head/Head.boundary.unit.test.ts | 24 + .../routing/Head/Head.negative.unit.test.ts | 14 + .../routing/Head/Head.positive.unit.test.ts | 58 +++ .../HeadMapping.boundary.unit.test.ts | 18 + .../HeadMapping.negative.unit.test.ts | 14 + .../HeadMapping.positive.unit.test.ts | 28 ++ .../Integration.positive.unit.test.ts | 30 ++ .../Options/Options.boundary.unit.test.ts | 24 + .../Options/Options.negative.unit.test.ts | 14 + .../Options/Options.positive.unit.test.ts | 58 +++ .../OptionsMapping.boundary.unit.test.ts | 18 + .../OptionsMapping.negative.unit.test.ts | 14 + .../OptionsMapping.positive.unit.test.ts | 28 ++ .../routing/Patch/Patch.boundary.unit.test.ts | 24 + .../routing/Patch/Patch.negative.unit.test.ts | 14 + .../routing/Patch/Patch.positive.unit.test.ts | 58 +++ .../PatchMapping.boundary.unit.test.ts | 18 + .../PatchMapping.negative.unit.test.ts | 14 + .../PatchMapping.positive.unit.test.ts | 28 ++ .../routing/Post/Post.boundary.unit.test.ts | 24 + .../routing/Post/Post.negative.unit.test.ts | 14 + .../routing/Post/Post.positive.unit.test.ts | 58 +++ .../PostMapping.boundary.unit.test.ts | 18 + .../PostMapping.negative.unit.test.ts | 14 + .../PostMapping.positive.unit.test.ts | 28 ++ .../routing/Put/Put.boundary.unit.test.ts | 24 + .../routing/Put/Put.negative.unit.test.ts | 14 + .../routing/Put/Put.positive.unit.test.ts | 58 +++ .../PutMapping.boundary.unit.test.ts | 18 + .../PutMapping.negative.unit.test.ts | 14 + .../PutMapping.positive.unit.test.ts | 28 ++ .../AppException.positive.unit.test.ts | 19 + .../HttpException.positive.unit.test.ts | 14 + .../MetadataRepository.extra.unit.test.ts | 29 ++ .../MetadataRepository.positive.unit.test.ts | 23 + ...assignInjectMetadata.positive.unit.test.ts | 14 + .../assignParamMetadata.positive.unit.test.ts | 17 + ...eAppsScriptDecorator.boundary.unit.test.ts | 21 + ...eAppsScriptDecorator.positive.unit.test.ts | 26 + .../createHttpDecorator.boundary.unit.test.ts | 35 ++ .../createHttpDecorator.positive.unit.test.ts | 24 + ...createParamDecorator.boundary.unit.test.ts | 36 ++ ...createParamDecorator.positive.unit.test.ts | 39 ++ .../getInjectionTokens.boundary.unit.test.ts | 19 + .../getInjectionTokens.positive.unit.test.ts | 31 ++ .../EventDispatcher.boundary.unit.test.ts | 32 ++ .../EventDispatcher.extra.unit.test.ts | 157 ++++++ .../EventDispatcher.negative.unit.test.ts | 50 ++ .../EventDispatcher.positive.unit.test.ts | 67 +++ .../PathMatcher.boundary.unit.test.ts | 21 + .../PathMatcher.negative.unit.test.ts | 18 + .../PathMatcher.positive.unit.test.ts | 35 ++ .../RequestFactory.boundary.unit.test.ts | 30 ++ .../RequestFactory.negative.unit.test.ts | 37 ++ .../RequestFactory.positive.unit.test.ts | 47 ++ .../Resolver/Resolver.boundary.unit.test.ts | 28 ++ .../Resolver/Resolver.extra.unit.test.ts | 41 ++ .../Resolver/Resolver.negative.unit.test.ts | 34 ++ .../Resolver/Resolver.positive.unit.test.ts | 51 ++ .../ResponseBuilder.extra.unit.test.ts | 72 +++ .../ResponseBuilder.negative.unit.test.ts | 19 + .../ResponseBuilder.positive.unit.test.ts | 70 +++ .../Router/Router.boundary.unit.test.ts | 56 +++ .../service/Router/Router.extra.unit.test.ts | 216 +++++++++ .../Router/Router.negative.unit.test.ts | 86 ++++ .../Router/Router.positive.unit.test.ts | 63 +++ .../RouterExplorer.boundary.unit.test.ts | 12 + .../RouterExplorer.negative.unit.test.ts | 22 + .../RouterExplorer.positive.unit.test.ts | 43 ++ test/unit/service/Services.extra.unit.test.ts | 45 ++ .../isController.boundary.unit.test.ts | 10 + .../isController.negative.unit.test.ts | 10 + .../isController.positive.unit.test.ts | 12 + .../isInjectable.boundary.unit.test.ts | 10 + .../isInjectable.negative.unit.test.ts | 10 + .../isInjectable.positive.unit.test.ts | 24 + 166 files changed, 4904 insertions(+), 841 deletions(-) delete mode 100644 test/decorators/class.test.ts delete mode 100644 test/decorators/method.test.ts delete mode 100644 test/decorators/param.test.ts create mode 100644 test/integration/BootApplication/BootApplication.boundary.integration.test.ts create mode 100644 test/integration/BootApplication/BootApplication.negative.integration.test.ts create mode 100644 test/integration/BootApplication/BootApplication.positive.integration.test.ts create mode 100644 test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts create mode 100644 test/unit/controller/BootApplicationFactory/BootApplicationFactory.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Controller/Controller.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Decorators.extra.unit.test.ts create mode 100644 test/unit/controller/decorators/Entity/Entity.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/Entity/Entity.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Entity/Entity.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/HttpController/HttpController.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Injectable/Injectable.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/Injectable/Injectable.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Injectable/Injectable.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Repository/Repository.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/Repository/Repository.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Repository/Repository.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/RestController/RestController.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/Service/Service.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/Service/Service.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/Service/Service.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnChange/OnChange.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnChange/OnChange.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnChange/OnChange.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnEdit/OnEdit.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnEdit/OnEdit.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnEdit/OnEdit.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnFormSubmit/OnFormSubmit.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnInstall/OnInstall.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnInstall/OnInstall.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnInstall/OnInstall.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnOpen/OnOpen.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnOpen/OnOpen.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/appsscript/OnOpen/OnOpen.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Body/Body.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Body/Body.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Body/Body.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Event/Event.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Event/Event.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Event/Event.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Headers/Headers.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Headers/Headers.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Headers/Headers.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Param/Param.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Param/Param.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Param/Param.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/PathVariable/PathVariable.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/PathVariable/PathVariable.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/PathVariable/PathVariable.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Query/Query.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Query/Query.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Query/Query.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Request/Request.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Request/Request.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Request/Request.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestBody/RequestBody.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestBody/RequestBody.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestBody/RequestBody.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestParam/RequestParam.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestParam/RequestParam.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/RequestParam/RequestParam.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Response/Response.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Response/Response.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/params/Response/Response.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Delete/Delete.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Delete/Delete.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Delete/Delete.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/DeleteMapping/DeleteMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Get/Get.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Get/Get.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Get/Get.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/GetMapping/GetMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/GetMapping/GetMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/GetMapping/GetMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Head/Head.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Head/Head.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Head/Head.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/HeadMapping/HeadMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/HeadMapping/HeadMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/HeadMapping/HeadMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Integration/Integration.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Options/Options.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Options/Options.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Options/Options.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/OptionsMapping/OptionsMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Patch/Patch.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Patch/Patch.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Patch/Patch.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PatchMapping/PatchMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PatchMapping/PatchMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PatchMapping/PatchMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Post/Post.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Post/Post.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Post/Post.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PostMapping/PostMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PostMapping/PostMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PostMapping/PostMapping.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Put/Put.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Put/Put.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/Put/Put.positive.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PutMapping/PutMapping.boundary.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PutMapping/PutMapping.negative.unit.test.ts create mode 100644 test/unit/controller/decorators/routing/PutMapping/PutMapping.positive.unit.test.ts create mode 100644 test/unit/exceptions/AppException/AppException.positive.unit.test.ts create mode 100644 test/unit/exceptions/HttpException/HttpException.positive.unit.test.ts create mode 100644 test/unit/repository/MetadataRepository/MetadataRepository.extra.unit.test.ts create mode 100644 test/unit/repository/MetadataRepository/MetadataRepository.positive.unit.test.ts create mode 100644 test/unit/repository/assignInjectMetadata/assignInjectMetadata.positive.unit.test.ts create mode 100644 test/unit/repository/assignParamMetadata/assignParamMetadata.positive.unit.test.ts create mode 100644 test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.boundary.unit.test.ts create mode 100644 test/unit/repository/createAppsScriptDecorator/createAppsScriptDecorator.positive.unit.test.ts create mode 100644 test/unit/repository/createHttpDecorator/createHttpDecorator.boundary.unit.test.ts create mode 100644 test/unit/repository/createHttpDecorator/createHttpDecorator.positive.unit.test.ts create mode 100644 test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts create mode 100644 test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts create mode 100644 test/unit/repository/getInjectionTokens/getInjectionTokens.boundary.unit.test.ts create mode 100644 test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts create mode 100644 test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts create mode 100644 test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts create mode 100644 test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts create mode 100644 test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts create mode 100644 test/unit/service/PathMatcher/PathMatcher.boundary.unit.test.ts create mode 100644 test/unit/service/PathMatcher/PathMatcher.negative.unit.test.ts create mode 100644 test/unit/service/PathMatcher/PathMatcher.positive.unit.test.ts create mode 100644 test/unit/service/RequestFactory/RequestFactory.boundary.unit.test.ts create mode 100644 test/unit/service/RequestFactory/RequestFactory.negative.unit.test.ts create mode 100644 test/unit/service/RequestFactory/RequestFactory.positive.unit.test.ts create mode 100644 test/unit/service/Resolver/Resolver.boundary.unit.test.ts create mode 100644 test/unit/service/Resolver/Resolver.extra.unit.test.ts create mode 100644 test/unit/service/Resolver/Resolver.negative.unit.test.ts create mode 100644 test/unit/service/Resolver/Resolver.positive.unit.test.ts create mode 100644 test/unit/service/ResponseBuilder/ResponseBuilder.extra.unit.test.ts create mode 100644 test/unit/service/ResponseBuilder/ResponseBuilder.negative.unit.test.ts create mode 100644 test/unit/service/ResponseBuilder/ResponseBuilder.positive.unit.test.ts create mode 100644 test/unit/service/Router/Router.boundary.unit.test.ts create mode 100644 test/unit/service/Router/Router.extra.unit.test.ts create mode 100644 test/unit/service/Router/Router.negative.unit.test.ts create mode 100644 test/unit/service/Router/Router.positive.unit.test.ts create mode 100644 test/unit/service/RouterExplorer/RouterExplorer.boundary.unit.test.ts create mode 100644 test/unit/service/RouterExplorer/RouterExplorer.negative.unit.test.ts create mode 100644 test/unit/service/RouterExplorer/RouterExplorer.positive.unit.test.ts create mode 100644 test/unit/service/Services.extra.unit.test.ts create mode 100644 test/unit/shared/utils/isController/isController.boundary.unit.test.ts create mode 100644 test/unit/shared/utils/isController/isController.negative.unit.test.ts create mode 100644 test/unit/shared/utils/isController/isController.positive.unit.test.ts create mode 100644 test/unit/shared/utils/isInjectable/isInjectable.boundary.unit.test.ts create mode 100644 test/unit/shared/utils/isInjectable/isInjectable.negative.unit.test.ts create mode 100644 test/unit/shared/utils/isInjectable/isInjectable.positive.unit.test.ts 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..c0a7dee --- /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..847a353 --- /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..2f5b860 --- /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..ef72d9f --- /dev/null +++ b/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts @@ -0,0 +1,25 @@ +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..b163355 --- /dev/null +++ b/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts @@ -0,0 +1,15 @@ +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..63442fe --- /dev/null +++ b/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts @@ -0,0 +1,59 @@ +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..df0ec13 --- /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..d641736 --- /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..a43d41c --- /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..40ed011 --- /dev/null +++ b/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts @@ -0,0 +1,18 @@ +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..18e48c8 --- /dev/null +++ b/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts @@ -0,0 +1,26 @@ +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..b08bb0d --- /dev/null +++ b/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts @@ -0,0 +1,57 @@ +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..a11fcf7 --- /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..54f0787 --- /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..bf3774e --- /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..be74462 --- /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..bf2f091 --- /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..03f9b13 --- /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..de63ff8 --- /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..08c62d6 --- /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..8f13451 --- /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); + }); +}); From ad52650208dca251c6224b716c5722dcacb201a8 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Fri, 6 Feb 2026 21:34:11 +0100 Subject: [PATCH 08/17] ci: update github workflows --- .github/workflows/codeql.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..55b79d4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '26 14 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: "ubuntu-latest" + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript-typescript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" From 8ae544fc540fde6335c25428e63c6eafae2534dd Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:10:37 +0100 Subject: [PATCH 09/17] chore: update build and configuration settings Co-authored-by: Junie --- config/eslint/index.ts | 20 +++--- config/typescript/tsconfig.appsscript.json | 73 +++++++++++++--------- config/typescript/tsconfig.base.json | 34 +++++++++- config/typescript/tsconfig.node.json | 29 ++++++--- package.json | 5 +- src/utils/index.ts | 23 +++++++ tsconfig.json | 28 ++++++++- 7 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 src/utils/index.ts diff --git a/config/eslint/index.ts b/config/eslint/index.ts index fc89271..3bf3b49 100644 --- a/config/eslint/index.ts +++ b/config/eslint/index.ts @@ -1,13 +1,13 @@ -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"; +import commonIgnores from "./common-ignores"; +import envAppsscript from "./env-appsscript"; +import langJavascript from "./lang-javascript"; +import langJson from "./lang-json"; +import langMarkdown from "./lang-markdown"; +import langTypescript from "./lang-typescript"; +import overridesTests from "./overrides-tests"; +import rulesJsdoc from "./rules-jsdoc"; +import rulesSpacing from "./rules-spacing"; +import rulesPrettier from "./rules-prettier"; /** * ESLint configurations entry point. diff --git a/config/typescript/tsconfig.appsscript.json b/config/typescript/tsconfig.appsscript.json index b267c41..2f50ad9 100644 --- a/config/typescript/tsconfig.appsscript.json +++ b/config/typescript/tsconfig.appsscript.json @@ -1,6 +1,15 @@ { - "extends": "./tsconfig.base.json", + /** + * 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 @@ -13,29 +22,17 @@ */ "allowUnusedLabels": false, - /** - * Base directory to resolve non-relative module names. - * @see https://www.typescriptlang.org/tsconfig#baseUrl - */ - "baseUrl": "../../src", - /** * Enable project compilation. * @see https://www.typescriptlang.org/tsconfig#composite */ - "composite": false, - - /** - * Emit design-type metadata for decorated declarations in source files. - * @see https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata - */ - "emitDecoratorMetadata": true, + "composite": true, /** - * Enable experimental support for legacy experimental decorators. - * @see https://www.typescriptlang.org/tsconfig#experimentalDecorators + * Generate .d.ts files from TypeScript and JavaScript files in your project. + * @see https://www.typescriptlang.org/tsconfig#declaration */ - "experimentalDecorators": true, + "declaration": true, /** * Ensure each file can be safely transpiled without relying on other imports. @@ -50,12 +47,22 @@ "lib": ["ESNext"], /** - * A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. - * @see https://www.typescriptlang.org/tsconfig#paths + * 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 */ - "paths": { - "src/*": ["./*"] - }, + "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. @@ -67,13 +74,13 @@ * List of folders to include type definitions from. * @see https://www.typescriptlang.org/tsconfig#typeRoots */ - "typeRoots": ["../../node_modules/@types", "../types"], + "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"], + "types": ["google-apps-script", "reflect-metadata"], /** * Emit ECMAScript-standard-compliant class fields. @@ -83,14 +90,20 @@ }, /** - * Specifies a list of glob patterns that match files to be included in compilation. - * @see https://www.typescriptlang.org/tsconfig#include + * Specifies a list of files to be excluded from compilation. + * @see https://www.typescriptlang.org/tsconfig#exclude */ - "include": ["../../src/**/*", "../../test/**/*", "../../config/**/*"], + "exclude": ["../../dist", "../../node_modules"], /** - * Specifies a list of files to be excluded from compilation. - * @see https://www.typescriptlang.org/tsconfig#exclude + * 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 */ - "exclude": ["../../node_modules", "../../dist"] + "include": ["../../src/**/*"] } diff --git a/config/typescript/tsconfig.base.json b/config/typescript/tsconfig.base.json index e1e8373..c875227 100644 --- a/config/typescript/tsconfig.base.json +++ b/config/typescript/tsconfig.base.json @@ -1,4 +1,8 @@ { + /** + * 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.). @@ -6,12 +10,30 @@ */ "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 @@ -66,6 +88,14 @@ */ "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 @@ -88,8 +118,6 @@ * Set the JavaScript language version for emitted JavaScript and include compatible library declarations. * @see https://www.typescriptlang.org/tsconfig#target */ - "target": "ESNext", - "experimentalDecorators": true, - "emitDecoratorMetadata": true + "target": "ESNext" } } diff --git a/config/typescript/tsconfig.node.json b/config/typescript/tsconfig.node.json index f0be3f6..53d6524 100644 --- a/config/typescript/tsconfig.node.json +++ b/config/typescript/tsconfig.node.json @@ -1,5 +1,8 @@ { - "extends": "./tsconfig.base.json", + /** + * Instructs the TypeScript compiler how to compile the .ts files. + * @see https://www.typescriptlang.org/tsconfig#compilerOptions + */ "compilerOptions": { /** * Ensure each file can be safely transpiled without relying on other imports. @@ -33,14 +36,26 @@ }, /** - * Specifies a list of glob patterns that match files to be included in compilation. - * @see https://www.typescriptlang.org/tsconfig#include + * Specifies a list of files to be excluded from compilation. + * @see https://www.typescriptlang.org/tsconfig#exclude */ - "include": ["../../config/**/*", "../../vite.config.ts"], + "exclude": ["../../dist", "../../node_modules"], /** - * Specifies a list of files to be excluded from compilation. - * @see https://www.typescriptlang.org/tsconfig#exclude + * 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 */ - "exclude": ["../../node_modules", "../../dist"] + "include": [ + "../../config/**/*", + "../../eslint.config.js", + "../../src/**/*", + "../../test/**/*", + "../../vitest.config.ts" + ] } diff --git a/package.json b/package.json index 27e87bf..1f9266e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "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", "scripts": { "prepare": "husky", "dev": "vitest", @@ -15,7 +16,7 @@ "test:unit": "vitest run test/unit", "test:integration": "vitest run test/integration", "test:cov": "npx vitest run --coverage", - "build": "rm -rf dist/*" + "build": "rm -rf dist/* && tsc -b --force" }, "repository": { "type": "git", diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..81df783 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,23 @@ +import { InjectionToken, Newable } from "../domain/types"; +import { Resolver, EventDispatcher } from "../service"; + +export function resolve( + controllers: Map, + providers: Map, + token: InjectionToken +): T { + const resolver = new Resolver(controllers, providers); + return resolver.resolve(token); +} + +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/tsconfig.json b/tsconfig.json index c783f04..a46800b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,32 @@ -// 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": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true + /** + * 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.appsscript.json" From 1e3331252f23a0a715555e424f86763e34c15b25 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:10:45 +0100 Subject: [PATCH 10/17] refactor: update decorators and their imports Co-authored-by: Junie --- src/controller/decorators/Controller.ts | 2 +- src/controller/decorators/Entity.ts | 2 +- src/controller/decorators/HttpController.ts | 2 +- src/controller/decorators/Inject.ts | 6 +++--- src/controller/decorators/Injectable.ts | 2 +- src/controller/decorators/Repository.ts | 2 +- src/controller/decorators/RestController.ts | 2 +- src/controller/decorators/Service.ts | 2 +- src/controller/decorators/appsscript/OnChange.ts | 4 ++-- src/controller/decorators/appsscript/OnEdit.ts | 4 ++-- src/controller/decorators/appsscript/OnFormSubmit.ts | 4 ++-- src/controller/decorators/appsscript/OnInstall.ts | 4 ++-- src/controller/decorators/appsscript/OnOpen.ts | 4 ++-- src/controller/decorators/params/Body.ts | 4 ++-- src/controller/decorators/params/Event.ts | 4 ++-- src/controller/decorators/params/Headers.ts | 4 ++-- src/controller/decorators/params/Param.ts | 4 ++-- src/controller/decorators/params/PathVariable.ts | 2 +- src/controller/decorators/params/Query.ts | 4 ++-- src/controller/decorators/params/Request.ts | 4 ++-- src/controller/decorators/params/RequestBody.ts | 2 +- src/controller/decorators/params/RequestParam.ts | 2 +- src/controller/decorators/params/Response.ts | 4 ++-- src/controller/decorators/routing/Delete.ts | 4 ++-- src/controller/decorators/routing/DeleteMapping.ts | 4 ++-- src/controller/decorators/routing/Get.ts | 4 ++-- src/controller/decorators/routing/GetMapping.ts | 4 ++-- src/controller/decorators/routing/Head.ts | 4 ++-- src/controller/decorators/routing/HeadMapping.ts | 4 ++-- src/controller/decorators/routing/Options.ts | 4 ++-- src/controller/decorators/routing/OptionsMapping.ts | 4 ++-- src/controller/decorators/routing/Patch.ts | 4 ++-- src/controller/decorators/routing/PatchMapping.ts | 4 ++-- src/controller/decorators/routing/Post.ts | 4 ++-- src/controller/decorators/routing/PostMapping.ts | 4 ++-- src/controller/decorators/routing/Put.ts | 4 ++-- src/controller/decorators/routing/PutMapping.ts | 4 ++-- 37 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/controller/decorators/Controller.ts b/src/controller/decorators/Controller.ts index a860c5f..05793e8 100644 --- a/src/controller/decorators/Controller.ts +++ b/src/controller/decorators/Controller.ts @@ -1,4 +1,4 @@ -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "domain/constants"; +import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "../../domain/constants"; /** * Controller options. diff --git a/src/controller/decorators/Entity.ts b/src/controller/decorators/Entity.ts index f4f287f..75e0ff1 100644 --- a/src/controller/decorators/Entity.ts +++ b/src/controller/decorators/Entity.ts @@ -1,4 +1,4 @@ -import { ENTITY_WATERMARK } from "domain/constants"; +import { ENTITY_WATERMARK } from "../../domain/constants"; /** * Decorator that marks a class as an entity. diff --git a/src/controller/decorators/HttpController.ts b/src/controller/decorators/HttpController.ts index eb94153..af9526b 100644 --- a/src/controller/decorators/HttpController.ts +++ b/src/controller/decorators/HttpController.ts @@ -1,4 +1,4 @@ -import { Controller } from "controller/decorators"; +import { Controller } from "../../controller/decorators"; /** * Decorator that marks a class as an HTTP controller. diff --git a/src/controller/decorators/Inject.ts b/src/controller/decorators/Inject.ts index bc71abc..3b0824d 100644 --- a/src/controller/decorators/Inject.ts +++ b/src/controller/decorators/Inject.ts @@ -1,6 +1,6 @@ -import { INJECT_TOKENS_METADATA } from "domain/constants"; -import { InjectTokenDefinition, Newable } from "domain/types"; -import { assignInjectMetadata } from "repository"; +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. diff --git a/src/controller/decorators/Injectable.ts b/src/controller/decorators/Injectable.ts index 0059a5d..90a84c6 100644 --- a/src/controller/decorators/Injectable.ts +++ b/src/controller/decorators/Injectable.ts @@ -1,4 +1,4 @@ -import { INJECTABLE_WATERMARK } from "domain/constants"; +import { INJECTABLE_WATERMARK } from "../../domain/constants"; /** * Decorator that marks a class as injectable (a provider). diff --git a/src/controller/decorators/Repository.ts b/src/controller/decorators/Repository.ts index dea75d4..e7f9f7a 100644 --- a/src/controller/decorators/Repository.ts +++ b/src/controller/decorators/Repository.ts @@ -1,4 +1,4 @@ -import { Injectable } from "controller/decorators"; +import { Injectable } from "../../controller/decorators"; /** * Decorator that marks a class as a repository. diff --git a/src/controller/decorators/RestController.ts b/src/controller/decorators/RestController.ts index ad796a4..942bcaf 100644 --- a/src/controller/decorators/RestController.ts +++ b/src/controller/decorators/RestController.ts @@ -1,3 +1,3 @@ -import { HttpController } from "controller/decorators"; +import { HttpController } from "../../controller/decorators"; export const RestController = HttpController; diff --git a/src/controller/decorators/Service.ts b/src/controller/decorators/Service.ts index 8c9f091..74568a3 100644 --- a/src/controller/decorators/Service.ts +++ b/src/controller/decorators/Service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "controller/decorators"; +import { Injectable } from "../../controller/decorators"; /** * Decorator that marks a class as a service. diff --git a/src/controller/decorators/appsscript/OnChange.ts b/src/controller/decorators/appsscript/OnChange.ts index 0851315..39c3923 100644 --- a/src/controller/decorators/appsscript/OnChange.ts +++ b/src/controller/decorators/appsscript/OnChange.ts @@ -1,4 +1,4 @@ -import { AppsScriptEventType } from "domain/enums"; -import { createAppsScriptDecorator } from "repository"; +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 index 46fba92..1248761 100644 --- a/src/controller/decorators/appsscript/OnEdit.ts +++ b/src/controller/decorators/appsscript/OnEdit.ts @@ -1,4 +1,4 @@ -import { AppsScriptEventType } from "domain/enums"; -import { createAppsScriptDecorator } from "repository"; +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 index 6696f86..be50c0a 100644 --- a/src/controller/decorators/appsscript/OnFormSubmit.ts +++ b/src/controller/decorators/appsscript/OnFormSubmit.ts @@ -1,4 +1,4 @@ -import { AppsScriptEventType } from "domain/enums"; -import { createAppsScriptDecorator } from "repository"; +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 index 6d1abfc..20060db 100644 --- a/src/controller/decorators/appsscript/OnInstall.ts +++ b/src/controller/decorators/appsscript/OnInstall.ts @@ -1,4 +1,4 @@ -import { AppsScriptEventType } from "domain/enums"; -import { createAppsScriptDecorator } from "repository"; +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 index 4b771f5..542be33 100644 --- a/src/controller/decorators/appsscript/OnOpen.ts +++ b/src/controller/decorators/appsscript/OnOpen.ts @@ -1,4 +1,4 @@ -import { AppsScriptEventType } from "domain/enums"; -import { createAppsScriptDecorator } from "repository"; +import { AppsScriptEventType } from "../../../domain/enums"; +import { createAppsScriptDecorator } from "../../../repository"; export const OnOpen = createAppsScriptDecorator(AppsScriptEventType.OPEN); diff --git a/src/controller/decorators/params/Body.ts b/src/controller/decorators/params/Body.ts index 69acce7..532b639 100644 --- a/src/controller/decorators/params/Body.ts +++ b/src/controller/decorators/params/Body.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting the full request body. diff --git a/src/controller/decorators/params/Event.ts b/src/controller/decorators/params/Event.ts index dd2348d..e171dbe 100644 --- a/src/controller/decorators/params/Event.ts +++ b/src/controller/decorators/params/Event.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting the raw Apps Script event object. diff --git a/src/controller/decorators/params/Headers.ts b/src/controller/decorators/params/Headers.ts index 2681348..cd85272 100644 --- a/src/controller/decorators/params/Headers.ts +++ b/src/controller/decorators/params/Headers.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting request headers. diff --git a/src/controller/decorators/params/Param.ts b/src/controller/decorators/params/Param.ts index c6cb054..6772e01 100644 --- a/src/controller/decorators/params/Param.ts +++ b/src/controller/decorators/params/Param.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting values from URL path parameters. diff --git a/src/controller/decorators/params/PathVariable.ts b/src/controller/decorators/params/PathVariable.ts index bdcd160..aa967c7 100644 --- a/src/controller/decorators/params/PathVariable.ts +++ b/src/controller/decorators/params/PathVariable.ts @@ -1,3 +1,3 @@ -import { Param } from "controller/decorators/params"; +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 index b38a01a..223652b 100644 --- a/src/controller/decorators/params/Query.ts +++ b/src/controller/decorators/params/Query.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting values from URL query parameters. diff --git a/src/controller/decorators/params/Request.ts b/src/controller/decorators/params/Request.ts index 4f57188..f92ac2a 100644 --- a/src/controller/decorators/params/Request.ts +++ b/src/controller/decorators/params/Request.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting the request object. diff --git a/src/controller/decorators/params/RequestBody.ts b/src/controller/decorators/params/RequestBody.ts index 6d817b4..b750254 100644 --- a/src/controller/decorators/params/RequestBody.ts +++ b/src/controller/decorators/params/RequestBody.ts @@ -1,3 +1,3 @@ -import { Body } from "controller/decorators/params"; +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 index 809a4c6..22287da 100644 --- a/src/controller/decorators/params/RequestParam.ts +++ b/src/controller/decorators/params/RequestParam.ts @@ -1,3 +1,3 @@ -import { Query } from "controller/decorators/params"; +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 index 8ac3e6a..257f906 100644 --- a/src/controller/decorators/params/Response.ts +++ b/src/controller/decorators/params/Response.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { createParamDecorator } from "repository"; +import { ParamSource } from "../../../domain/enums"; +import { createParamDecorator } from "../../../repository"; /** * A parameter decorator for injecting the response object. diff --git a/src/controller/decorators/routing/Delete.ts b/src/controller/decorators/routing/Delete.ts index dcdb963..f98aafb 100644 --- a/src/controller/decorators/routing/Delete.ts +++ b/src/controller/decorators/routing/Delete.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP DELETE requests. diff --git a/src/controller/decorators/routing/DeleteMapping.ts b/src/controller/decorators/routing/DeleteMapping.ts index da9cf30..7ba5668 100644 --- a/src/controller/decorators/routing/DeleteMapping.ts +++ b/src/controller/decorators/routing/DeleteMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP DELETE requests. diff --git a/src/controller/decorators/routing/Get.ts b/src/controller/decorators/routing/Get.ts index a245a4c..a51e72a 100644 --- a/src/controller/decorators/routing/Get.ts +++ b/src/controller/decorators/routing/Get.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP GET requests. diff --git a/src/controller/decorators/routing/GetMapping.ts b/src/controller/decorators/routing/GetMapping.ts index 403a406..8917900 100644 --- a/src/controller/decorators/routing/GetMapping.ts +++ b/src/controller/decorators/routing/GetMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP GET requests. diff --git a/src/controller/decorators/routing/Head.ts b/src/controller/decorators/routing/Head.ts index b43c5f8..7662b89 100644 --- a/src/controller/decorators/routing/Head.ts +++ b/src/controller/decorators/routing/Head.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP HEAD requests. diff --git a/src/controller/decorators/routing/HeadMapping.ts b/src/controller/decorators/routing/HeadMapping.ts index 123cef3..0fac6b3 100644 --- a/src/controller/decorators/routing/HeadMapping.ts +++ b/src/controller/decorators/routing/HeadMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP HEAD requests. diff --git a/src/controller/decorators/routing/Options.ts b/src/controller/decorators/routing/Options.ts index d563d07..2792c7f 100644 --- a/src/controller/decorators/routing/Options.ts +++ b/src/controller/decorators/routing/Options.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP OPTIONS requests. diff --git a/src/controller/decorators/routing/OptionsMapping.ts b/src/controller/decorators/routing/OptionsMapping.ts index 701c425..886fc88 100644 --- a/src/controller/decorators/routing/OptionsMapping.ts +++ b/src/controller/decorators/routing/OptionsMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP OPTIONS requests. diff --git a/src/controller/decorators/routing/Patch.ts b/src/controller/decorators/routing/Patch.ts index 98d16bf..27302b1 100644 --- a/src/controller/decorators/routing/Patch.ts +++ b/src/controller/decorators/routing/Patch.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP PATCH requests. diff --git a/src/controller/decorators/routing/PatchMapping.ts b/src/controller/decorators/routing/PatchMapping.ts index b4ba42f..c011219 100644 --- a/src/controller/decorators/routing/PatchMapping.ts +++ b/src/controller/decorators/routing/PatchMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP PATCH requests. diff --git a/src/controller/decorators/routing/Post.ts b/src/controller/decorators/routing/Post.ts index b066bbb..61b5296 100644 --- a/src/controller/decorators/routing/Post.ts +++ b/src/controller/decorators/routing/Post.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP POST requests. diff --git a/src/controller/decorators/routing/PostMapping.ts b/src/controller/decorators/routing/PostMapping.ts index 9d4332a..2a8ee1a 100644 --- a/src/controller/decorators/routing/PostMapping.ts +++ b/src/controller/decorators/routing/PostMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP POST requests. diff --git a/src/controller/decorators/routing/Put.ts b/src/controller/decorators/routing/Put.ts index 0cba2d1..e47da52 100644 --- a/src/controller/decorators/routing/Put.ts +++ b/src/controller/decorators/routing/Put.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP PUT requests. diff --git a/src/controller/decorators/routing/PutMapping.ts b/src/controller/decorators/routing/PutMapping.ts index 40cc75c..85e0ffc 100644 --- a/src/controller/decorators/routing/PutMapping.ts +++ b/src/controller/decorators/routing/PutMapping.ts @@ -1,5 +1,5 @@ -import { RequestMethod } from "domain/enums"; -import { createHttpDecorator } from "repository"; +import { RequestMethod } from "../../../domain/enums"; +import { createHttpDecorator } from "../../../repository"; /** * Route handler decorator for HTTP PUT requests. From cd60b5a92d76306aedae2cc626f937d856f6ed5c Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:10:49 +0100 Subject: [PATCH 11/17] refactor: update domain types and entities Co-authored-by: Junie --- src/domain/entities/RouteExecutionContext.ts | 2 +- src/domain/types/HttpRequest.ts | 2 +- src/domain/types/HttpResponse.ts | 2 +- src/domain/types/InjectTokenDefinition.ts | 2 +- src/domain/types/ParamDefinition.ts | 2 +- src/domain/types/RouteMetadata.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/domain/entities/RouteExecutionContext.ts b/src/domain/entities/RouteExecutionContext.ts index b186f30..5b5df41 100644 --- a/src/domain/entities/RouteExecutionContext.ts +++ b/src/domain/entities/RouteExecutionContext.ts @@ -1,4 +1,4 @@ -import { HttpHeaders, HttpRequest, HttpResponse, ParsedUrlQuery } from "domain/types"; +import { HttpHeaders, HttpRequest, HttpResponse, ParsedUrlQuery } from "../../domain/types"; /** * Execution context for a route handler. diff --git a/src/domain/types/HttpRequest.ts b/src/domain/types/HttpRequest.ts index c5c0409..6f2f4eb 100644 --- a/src/domain/types/HttpRequest.ts +++ b/src/domain/types/HttpRequest.ts @@ -1,6 +1,6 @@ import { HttpHeaders } from "./HttpHeaders"; import { ParsedUrl } from "./ParsedUrl"; -import { RequestMethod } from "domain/enums"; +import { RequestMethod } from "../../domain/enums"; export interface HttpRequest { headers: HttpHeaders; diff --git a/src/domain/types/HttpResponse.ts b/src/domain/types/HttpResponse.ts index 824c470..c85ffa1 100644 --- a/src/domain/types/HttpResponse.ts +++ b/src/domain/types/HttpResponse.ts @@ -1,5 +1,5 @@ import { HttpHeaders } from "./HttpHeaders"; -import { HttpStatus } from "domain/enums"; +import { HttpStatus } from "../../domain/enums"; export interface HttpResponse { headers: HttpHeaders; diff --git a/src/domain/types/InjectTokenDefinition.ts b/src/domain/types/InjectTokenDefinition.ts index 5d1b041..9381e1c 100644 --- a/src/domain/types/InjectTokenDefinition.ts +++ b/src/domain/types/InjectTokenDefinition.ts @@ -1,4 +1,4 @@ -import { ParamSource } from "domain/enums"; +import { ParamSource } from "../../domain/enums"; import { InjectionToken } from "./InjectionToken"; export interface InjectTokenDefinition { diff --git a/src/domain/types/ParamDefinition.ts b/src/domain/types/ParamDefinition.ts index d7257e6..abb96e1 100644 --- a/src/domain/types/ParamDefinition.ts +++ b/src/domain/types/ParamDefinition.ts @@ -1,4 +1,4 @@ -import { ParamSource } from "domain/enums"; +import { ParamSource } from "../../domain/enums"; export interface ParamDefinition { type: ParamSource; diff --git a/src/domain/types/RouteMetadata.ts b/src/domain/types/RouteMetadata.ts index 3475a35..7bc12c7 100644 --- a/src/domain/types/RouteMetadata.ts +++ b/src/domain/types/RouteMetadata.ts @@ -1,5 +1,5 @@ import { Newable } from "./Newable"; -import { RequestMethod } from "domain/enums"; +import { RequestMethod } from "../../domain/enums"; export interface RouteMetadata { controller: Newable; From 7bacf9c12cb66ed5b44dbe3b887793e9895bcdd7 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:10:53 +0100 Subject: [PATCH 12/17] refactor: update services and their imports Co-authored-by: Junie --- src/service/EventDispatcher.ts | 126 +++++++++++++++++---------------- src/service/RequestFactory.ts | 4 +- src/service/Resolver.ts | 10 +-- src/service/ResponseBuilder.ts | 4 +- src/service/Router.ts | 12 ++-- src/service/RouterExplorer.ts | 4 +- 6 files changed, 82 insertions(+), 78 deletions(-) diff --git a/src/service/EventDispatcher.ts b/src/service/EventDispatcher.ts index 1d22b20..820150d 100644 --- a/src/service/EventDispatcher.ts +++ b/src/service/EventDispatcher.ts @@ -3,11 +3,11 @@ import { 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"; +} 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( @@ -24,7 +24,7 @@ export class EventDispatcher { for (const propertyName of propertyNames) { if (propertyName === "constructor") continue; - const methodHandler = prototype[ propertyName ]; + const methodHandler = prototype[propertyName]; const eventMetadata = Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodHandler); @@ -33,9 +33,9 @@ export class EventDispatcher { if (eventMetadata === eventType && this.checkFilters(eventType, event, options)) { const instance = this.resolver.resolve(controller); - const args = this.buildArgs(instance as object, propertyName, event); + const args = this.buildMethodParams(instance as object, propertyName, event); - const handler = (instance as Record)[ propertyName ] as ( + const handler = (instance as Record)[propertyName] as ( ...args: unknown[] ) => unknown; await handler.apply(instance, args); @@ -44,6 +44,60 @@ export class EventDispatcher { } } + 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, @@ -64,7 +118,7 @@ export class EventDispatcher { return false; } - const ranges = Array.isArray(options.range) ? options.range : [ options.range ]; + const ranges = Array.isArray(options.range) ? options.range : [options.range]; // TODO: isRegExp return ranges.some((r: string | RegExp) => @@ -84,7 +138,7 @@ export class EventDispatcher { return false; } - const formIds = Array.isArray(options.formId) ? options.formId : [ options.formId ]; + const formIds = Array.isArray(options.formId) ? options.formId : [options.formId]; return formIds.some((id: string) => eventFormId === id); } @@ -101,7 +155,7 @@ export class EventDispatcher { const changeTypes = Array.isArray(options.changeType) ? options.changeType - : [ options.changeType ]; + : [options.changeType]; return changeTypes.some((type: unknown) => eventChangeType === type); } @@ -109,54 +163,4 @@ export class EventDispatcher { } return true; } - - private buildArgs(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; - } } diff --git a/src/service/RequestFactory.ts b/src/service/RequestFactory.ts index 2d3ba52..17c7c4f 100644 --- a/src/service/RequestFactory.ts +++ b/src/service/RequestFactory.ts @@ -1,6 +1,6 @@ import { isString, normalize } from "apps-script-utils"; -import { HttpHeaders, HttpRequest, ParsedUrl } from "domain/types"; -import { RequestMethod } from "domain/enums"; +import { HttpHeaders, HttpRequest, ParsedUrl } from "../domain/types"; +import { RequestMethod } from "../domain/enums"; export class RequestFactory { /** diff --git a/src/service/Resolver.ts b/src/service/Resolver.ts index b7b98af..6dec67c 100644 --- a/src/service/Resolver.ts +++ b/src/service/Resolver.ts @@ -1,9 +1,9 @@ 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"; +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( diff --git a/src/service/ResponseBuilder.ts b/src/service/ResponseBuilder.ts index ad3b4b4..86258ba 100644 --- a/src/service/ResponseBuilder.ts +++ b/src/service/ResponseBuilder.ts @@ -1,5 +1,5 @@ -import { HeaderAcceptMimeType, HttpStatus, RequestMethod } from "domain/enums"; -import { HttpHeaders, HttpRequest, HttpResponse } from "domain/types"; +import { HeaderAcceptMimeType, HttpStatus, RequestMethod } from "../domain/enums"; +import { HttpHeaders, HttpRequest, HttpResponse } from "../domain/types"; export class ResponseBuilder { /** diff --git a/src/service/Router.ts b/src/service/Router.ts index 879ba3e..eab3163 100644 --- a/src/service/Router.ts +++ b/src/service/Router.ts @@ -7,12 +7,12 @@ import { 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"; +} 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(); diff --git a/src/service/RouterExplorer.ts b/src/service/RouterExplorer.ts index 85c8e83..b2b0976 100644 --- a/src/service/RouterExplorer.ts +++ b/src/service/RouterExplorer.ts @@ -1,6 +1,6 @@ import { normalize } from "apps-script-utils"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, METHOD_METADATA, PATH_METADATA } from "domain/constants"; -import { Newable, RouteMetadata } from "domain/types"; +import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, METHOD_METADATA, PATH_METADATA } from "../domain/constants"; +import { Newable, RouteMetadata } from "../domain/types"; export class RouterExplorer { public explore(controllers: Map): RouteMetadata[] { From ed3c576c201292982bf539016de00ed52994af1a Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:11:02 +0100 Subject: [PATCH 13/17] refactor: update repositories, controllers, and utilities Co-authored-by: Junie --- src/controller/BootApplication.ts | 15 ++++----------- src/controller/BootApplicationFactory.ts | 4 ++-- src/exceptions/HttpException.ts | 4 ++-- src/index.ts | 7 +++++-- src/repository/assignInjectMetadata.ts | 4 ++-- src/repository/assignParamMetadata.ts | 4 ++-- src/repository/createAppsScriptDecorator.ts | 4 ++-- src/repository/createHttpDecorator.ts | 4 ++-- src/repository/createMethodDecorator.ts | 4 ++-- src/repository/createParamDecorator.ts | 8 ++++---- src/repository/getInjectionTokens.ts | 4 ++-- src/shared/utils/createRequest.ts | 4 ++-- src/shared/utils/createResponse.ts | 4 ++-- src/shared/utils/isController.ts | 4 ++-- src/shared/utils/isInjectable.ts | 4 ++-- src/shared/utils/wrapResponse.ts | 4 ++-- src/utils/index.ts | 2 +- 17 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/controller/BootApplication.ts b/src/controller/BootApplication.ts index 1269314..ad5fbea 100644 --- a/src/controller/BootApplication.ts +++ b/src/controller/BootApplication.ts @@ -1,13 +1,6 @@ -import { ApplicationConfig, InjectionToken, Newable } from "domain/types"; -import { AppsScriptEventType, RequestMethod } from "domain/enums"; -import { - EventDispatcher, - RequestFactory, - Resolver, - ResponseBuilder, - Router, - RouterExplorer -} from "service"; +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(); @@ -74,7 +67,7 @@ export class BootApplication { await this._eventDispatcher.dispatch(AppsScriptEventType.CHANGE, event); } - // TODO + // TODO: onSelectionChange // public async onSelectionChange(event: GoogleAppsScript.Events.SheetsOnSelectionChange) { // await this.eventDispatcher.dispatch(AppsScriptEventType.SELECTION_CHANGE, event); // } diff --git a/src/controller/BootApplicationFactory.ts b/src/controller/BootApplicationFactory.ts index 721f915..b0f387a 100644 --- a/src/controller/BootApplicationFactory.ts +++ b/src/controller/BootApplicationFactory.ts @@ -1,5 +1,5 @@ -import { ApplicationConfig } from "domain/types"; -import { BootApplication } from "controller"; +import { ApplicationConfig } from "../domain/types"; +import { BootApplication } from "../controller"; export class BootApplicationFactory { /** diff --git a/src/exceptions/HttpException.ts b/src/exceptions/HttpException.ts index cbd274f..3c74481 100644 --- a/src/exceptions/HttpException.ts +++ b/src/exceptions/HttpException.ts @@ -1,5 +1,5 @@ -import { HttpStatus } from "domain/enums"; -import { AppException } from "exceptions"; +import { HttpStatus } from "../domain/enums"; +import { AppException } from "../exceptions"; export class HttpException extends AppException { constructor( diff --git a/src/index.ts b/src/index.ts index 26cbed7..5ae5c4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ -import { BootApplication, BootApplicationFactory } from "controller"; +import { BootApplication, BootApplicationFactory } from "./controller"; -export * from "controller"; +export * from "./controller"; +export * from "./utils"; +export * from "./domain/types"; +export * from "./domain/enums"; export { BootApplication as App }; export const createApp = BootApplicationFactory.create; diff --git a/src/repository/assignInjectMetadata.ts b/src/repository/assignInjectMetadata.ts index 947dc98..ec8e1a6 100644 --- a/src/repository/assignInjectMetadata.ts +++ b/src/repository/assignInjectMetadata.ts @@ -1,5 +1,5 @@ -import { ParamSource } from "domain/enums"; -import { InjectionToken, InjectTokenDefinition } from "domain/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. diff --git a/src/repository/assignParamMetadata.ts b/src/repository/assignParamMetadata.ts index 99d25a3..682b222 100644 --- a/src/repository/assignParamMetadata.ts +++ b/src/repository/assignParamMetadata.ts @@ -1,5 +1,5 @@ -import { ParamDefinition } from "domain/types"; -import { ParamSource } from "domain/enums"; +import { ParamDefinition } from "../domain/types"; +import { ParamSource } from "../domain/enums"; /** * Updates parameter metadata with the argument's position (index). diff --git a/src/repository/createAppsScriptDecorator.ts b/src/repository/createAppsScriptDecorator.ts index a5ad98b..1538f3d 100644 --- a/src/repository/createAppsScriptDecorator.ts +++ b/src/repository/createAppsScriptDecorator.ts @@ -1,5 +1,5 @@ -import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "domain/constants"; -import { AppsScriptEventType } from "domain/enums"; +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. diff --git a/src/repository/createHttpDecorator.ts b/src/repository/createHttpDecorator.ts index 1dc5946..8ac1e70 100644 --- a/src/repository/createHttpDecorator.ts +++ b/src/repository/createHttpDecorator.ts @@ -1,5 +1,5 @@ -import { METHOD_METADATA, PATH_METADATA } from "domain/constants"; -import { RequestMethod } from "domain/enums"; +import { METHOD_METADATA, PATH_METADATA } from "../domain/constants"; +import { RequestMethod } from "../domain/enums"; /** * A factory function that creates method decorators for HTTP methods. diff --git a/src/repository/createMethodDecorator.ts b/src/repository/createMethodDecorator.ts index 4c8be22..2687111 100644 --- a/src/repository/createMethodDecorator.ts +++ b/src/repository/createMethodDecorator.ts @@ -1,5 +1,5 @@ -import { AppsScriptEventType } from "domain/enums"; -import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "domain/constants"; +import { AppsScriptEventType } from "../domain/enums"; +import { APPSSCRIPT_EVENT_METADATA, APPSSCRIPT_OPTIONS_METADATA } from "../domain/constants"; /** * Options for Google Apps Script events. diff --git a/src/repository/createParamDecorator.ts b/src/repository/createParamDecorator.ts index 454814d..d4982b3 100644 --- a/src/repository/createParamDecorator.ts +++ b/src/repository/createParamDecorator.ts @@ -1,7 +1,7 @@ -import { PARAM_DEFINITIONS_METADATA } from "domain/constants"; -import { ParamDefinition } from "domain/types"; -import { ParamSource } from "domain/enums"; -import { assignParamMetadata } from "repository"; +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. diff --git a/src/repository/getInjectionTokens.ts b/src/repository/getInjectionTokens.ts index a88f153..2821243 100644 --- a/src/repository/getInjectionTokens.ts +++ b/src/repository/getInjectionTokens.ts @@ -1,5 +1,5 @@ -import { INJECT_TOKENS_METADATA } from "domain/constants"; -import { InjectTokenDefinition } from "domain/types"; +import { INJECT_TOKENS_METADATA } from "../domain/constants"; +import { InjectTokenDefinition } from "../domain/types"; /** * Retrieves injection tokens associated with a class constructor or a method prototype. diff --git a/src/shared/utils/createRequest.ts b/src/shared/utils/createRequest.ts index 029c371..ddcb88b 100644 --- a/src/shared/utils/createRequest.ts +++ b/src/shared/utils/createRequest.ts @@ -1,6 +1,6 @@ import { isString, normalize } from "apps-script-utils"; -import { HttpHeaders, HttpRequest, ParsedUrl } from "domain/types"; -import { RequestMethod } from "domain/enums"; +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. diff --git a/src/shared/utils/createResponse.ts b/src/shared/utils/createResponse.ts index 7e372d3..8df9f1e 100644 --- a/src/shared/utils/createResponse.ts +++ b/src/shared/utils/createResponse.ts @@ -1,5 +1,5 @@ -import { HttpHeaders, HttpRequest, HttpResponse } from "domain/types"; -import { HttpStatus, RequestMethod } from "domain/enums"; +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. diff --git a/src/shared/utils/isController.ts b/src/shared/utils/isController.ts index 2911115..fb58f55 100644 --- a/src/shared/utils/isController.ts +++ b/src/shared/utils/isController.ts @@ -1,5 +1,5 @@ -import { CONTROLLER_WATERMARK } from "domain/constants"; -import { Newable } from "domain/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/shared/utils/isInjectable.ts b/src/shared/utils/isInjectable.ts index 0a9df11..c80830a 100644 --- a/src/shared/utils/isInjectable.ts +++ b/src/shared/utils/isInjectable.ts @@ -1,5 +1,5 @@ -import { INJECTABLE_WATERMARK } from "domain/constants"; -import { Newable } from "domain/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/shared/utils/wrapResponse.ts b/src/shared/utils/wrapResponse.ts index 6445b2c..1144309 100644 --- a/src/shared/utils/wrapResponse.ts +++ b/src/shared/utils/wrapResponse.ts @@ -1,5 +1,5 @@ -import { HeaderAcceptMimeType } from "domain/enums"; -import { HttpRequest, HttpResponse } from "domain/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`). diff --git a/src/utils/index.ts b/src/utils/index.ts index 81df783..b812aca 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ import { InjectionToken, Newable } from "../domain/types"; -import { Resolver, EventDispatcher } from "../service"; +import { EventDispatcher, Resolver } from "../service"; export function resolve( controllers: Map, From d9304716a143af7f96645f474d110beb9ad74214 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:14:32 +0100 Subject: [PATCH 14/17] fix --- .junie/guidelines.md | 95 ++++++++++++++ config/eslint/index.ts | 20 +-- package-lock.json | 226 ++++++++++++++++----------------- src/service/EventDispatcher.ts | 22 ++-- 4 files changed, 229 insertions(+), 134 deletions(-) create mode 100644 .junie/guidelines.md 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/config/eslint/index.ts b/config/eslint/index.ts index 3bf3b49..fc89271 100644 --- a/config/eslint/index.ts +++ b/config/eslint/index.ts @@ -1,13 +1,13 @@ -import commonIgnores from "./common-ignores"; -import envAppsscript from "./env-appsscript"; -import langJavascript from "./lang-javascript"; -import langJson from "./lang-json"; -import langMarkdown from "./lang-markdown"; -import langTypescript from "./lang-typescript"; -import overridesTests from "./overrides-tests"; -import rulesJsdoc from "./rules-jsdoc"; -import rulesSpacing from "./rules-spacing"; -import rulesPrettier from "./rules-prettier"; +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. diff --git a/package-lock.json b/package-lock.json index b625b06..5ec751a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -872,9 +872,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "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" ], @@ -886,9 +886,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "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" ], @@ -900,9 +900,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "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" ], @@ -914,9 +914,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "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" ], @@ -928,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "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" ], @@ -942,9 +942,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "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" ], @@ -956,9 +956,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "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" ], @@ -970,9 +970,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "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" ], @@ -984,9 +984,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "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" ], @@ -998,9 +998,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "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" ], @@ -1012,9 +1012,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "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" ], @@ -1026,9 +1026,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "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" ], @@ -1040,9 +1040,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "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" ], @@ -1054,9 +1054,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "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" ], @@ -1068,9 +1068,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "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" ], @@ -1082,9 +1082,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "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" ], @@ -1096,9 +1096,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "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" ], @@ -1110,9 +1110,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "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" ], @@ -1124,9 +1124,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "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" ], @@ -1138,9 +1138,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "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" ], @@ -1152,9 +1152,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "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" ], @@ -1166,9 +1166,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "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" ], @@ -1180,9 +1180,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "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" ], @@ -1194,9 +1194,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "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" ], @@ -1208,9 +1208,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "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" ], @@ -1523,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" @@ -1746,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": { @@ -3669,9 +3669,9 @@ "license": "MIT" }, "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": { @@ -3954,9 +3954,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "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": { @@ -3970,31 +3970,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@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" } }, diff --git a/src/service/EventDispatcher.ts b/src/service/EventDispatcher.ts index 820150d..7b18354 100644 --- a/src/service/EventDispatcher.ts +++ b/src/service/EventDispatcher.ts @@ -24,7 +24,7 @@ export class EventDispatcher { for (const propertyName of propertyNames) { if (propertyName === "constructor") continue; - const methodHandler = prototype[propertyName]; + const methodHandler = prototype[ propertyName ]; const eventMetadata = Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodHandler); @@ -35,7 +35,7 @@ export class EventDispatcher { const args = this.buildMethodParams(instance as object, propertyName, event); - const handler = (instance as Record)[propertyName] as ( + const handler = (instance as Record)[ propertyName ] as ( ...args: unknown[] ) => unknown; await handler.apply(instance, args); @@ -73,23 +73,23 @@ export class EventDispatcher { for (const param of metadata) { switch (param.type) { case ParamSource.EVENT: - args[param.index] = + args[ param.index ] = param.key && typeof event === "object" && event !== null - ? (event as Record)[param.key] + ? (event as Record)[ param.key ] : event; break; case ParamSource.INJECT: try { - const tokenToResolve = "token" in param ? param.token : designParamTypes[param.index]; + const tokenToResolve = "token" in param ? param.token : designParamTypes[ param.index ]; if (tokenToResolve) { - args[param.index] = this.resolver.resolve(tokenToResolve); + args[ param.index ] = this.resolver.resolve(tokenToResolve); } else { - args[param.index] = undefined; + args[ param.index ] = undefined; } } catch { - args[param.index] = undefined; + args[ param.index ] = undefined; } break; } @@ -118,7 +118,7 @@ export class EventDispatcher { return false; } - const ranges = Array.isArray(options.range) ? options.range : [options.range]; + const ranges = Array.isArray(options.range) ? options.range : [ options.range ]; // TODO: isRegExp return ranges.some((r: string | RegExp) => @@ -138,7 +138,7 @@ export class EventDispatcher { return false; } - const formIds = Array.isArray(options.formId) ? options.formId : [options.formId]; + const formIds = Array.isArray(options.formId) ? options.formId : [ options.formId ]; return formIds.some((id: string) => eventFormId === id); } @@ -155,7 +155,7 @@ export class EventDispatcher { const changeTypes = Array.isArray(options.changeType) ? options.changeType - : [options.changeType]; + : [ options.changeType ]; return changeTypes.some((type: unknown) => eventChangeType === type); } From a2fff876ae7c4b1c7bae7a982b85f52123c6d946 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:16:40 +0100 Subject: [PATCH 15/17] fix --- scripts/pre-commit.sh | 4 ++-- scripts/pre-push.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 From 9df32d6a18dd4bbf59e22e6074238f78650133a2 Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:20:56 +0100 Subject: [PATCH 16/17] fix --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 1f9266e..7052d80 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "Boot Framework for Google Apps Script™ projects.", "main": "dist/index.js", "types": "dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "prepare": "husky", "dev": "vitest", From 4add0c960dc5a787626dd380e9290dbac04fe38b Mon Sep 17 00:00:00 2001 From: Maksym Stoianov Date: Wed, 11 Mar 2026 17:27:19 +0100 Subject: [PATCH 17/17] fix --- .github/dependabot.yml | 18 --------- .github/workflows/codeql.yml | 40 ------------------- README.md | 15 ++----- config/eslint/common-ignores.ts | 2 +- config/eslint/env-appsscript.ts | 2 +- config/eslint/lang-javascript.ts | 2 +- config/eslint/lang-json.ts | 8 ++-- config/eslint/lang-markdown.ts | 4 +- config/eslint/lang-typescript.ts | 2 +- config/eslint/overrides-tests.ts | 2 +- config/eslint/rules-jsdoc.ts | 2 +- config/eslint/rules-spacing.ts | 16 ++++---- config/vite/types/BuildOptions.ts | 1 - config/vite/types/BuildPaths.ts | 1 - config/vite/types/ResolveOptions.ts | 1 - config/vite/utils/buildAliases.ts | 2 +- package.json | 7 ++-- src/controller/BootApplication.ts | 9 ++++- src/controller/decorators/Controller.ts | 6 ++- src/domain/enums/AppsScriptEventType.ts | 1 - src/domain/enums/HeaderAcceptMimeType.ts | 1 - src/repository/assignInjectMetadata.ts | 2 +- src/repository/assignParamMetadata.ts | 2 +- src/service/EventDispatcher.ts | 22 +++++----- src/service/PathMatcher.ts | 4 +- src/service/RequestFactory.ts | 2 +- src/service/Resolver.ts | 6 +-- src/service/ResponseBuilder.ts | 8 ++-- src/service/Router.ts | 34 ++++++++-------- src/service/RouterExplorer.ts | 9 ++++- src/shared/utils/createResponse.ts | 10 ++--- src/shared/utils/extractPathParams.ts | 7 +--- src/shared/utils/pathMatch.ts | 2 +- src/shared/utils/wrapResponse.ts | 19 +++------ ...otApplication.negative.integration.test.ts | 4 +- ...otApplication.positive.integration.test.ts | 10 ++--- .../BootApplication.positive.unit.test.ts | 2 +- .../Controller.positive.unit.test.ts | 6 ++- .../HttpController.negative.unit.test.ts | 6 ++- .../HttpController.positive.unit.test.ts | 6 ++- .../Inject/Inject.boundary.unit.test.ts | 4 +- .../Inject/Inject.negative.unit.test.ts | 2 +- .../Inject/Inject.positive.unit.test.ts | 4 +- .../RestController.boundary.unit.test.ts | 6 ++- .../RestController.positive.unit.test.ts | 6 ++- .../Integration.positive.unit.test.ts | 10 ++++- ...createParamDecorator.boundary.unit.test.ts | 6 +-- ...createParamDecorator.positive.unit.test.ts | 4 +- .../getInjectionTokens.positive.unit.test.ts | 4 +- .../EventDispatcher.boundary.unit.test.ts | 4 +- .../EventDispatcher.extra.unit.test.ts | 14 +++---- .../EventDispatcher.negative.unit.test.ts | 4 +- .../EventDispatcher.positive.unit.test.ts | 4 +- .../Resolver/Resolver.extra.unit.test.ts | 4 +- test/unit/service/Services.extra.unit.test.ts | 4 +- 55 files changed, 170 insertions(+), 213 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql.yml 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/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 55b79d4..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '26 14 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: "ubuntu-latest" - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript-typescript' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" diff --git a/README.md b/README.md index 1d20944..b94a3d1 100644 --- a/README.md +++ b/README.md @@ -35,16 +35,7 @@ aid in code organization. Install the framework via npm: ```bash -npm install github:bootgs/boot#main -``` - -> [!TIP] -> Use specific tags (e.g., `#v1.1.0`) in production for stability. - -For example: - -```bash -npm install github:bootgs/boot#v1.1.0 +npm install bootgs ``` ## Quick Start @@ -55,7 +46,7 @@ Create a class to handle your application's logic. Decorators make it easy to ma events. ```TypeScript -import {Get, RestController} from "boot"; +import {Get, RestController} from "bootgs"; @RestController("api/sheet") export class SheetController { @@ -75,7 +66,7 @@ Bootstrap your application by creating an `App` instance and delegating the stan `doPost`) to it. ```TypeScript -import {App} from "boot"; +import {App} from "bootgs"; import {SheetController} from "./SheetController"; /** diff --git a/config/eslint/common-ignores.ts b/config/eslint/common-ignores.ts index 5646aca..7ff443f 100644 --- a/config/eslint/common-ignores.ts +++ b/config/eslint/common-ignores.ts @@ -6,7 +6,7 @@ import type { Linter } from "eslint"; * @see https://eslint.org/docs/latest/use/configure/ignore */ const config: Linter.Config = { - ignores: [ "dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js" ] + ignores: ["dist/*", "package-lock.json", "tsconfig*.json", "src/**/*.js"] }; export default config; diff --git a/config/eslint/env-appsscript.ts b/config/eslint/env-appsscript.ts index 4b7e8bd..fca36b9 100644 --- a/config/eslint/env-appsscript.ts +++ b/config/eslint/env-appsscript.ts @@ -4,7 +4,7 @@ import type { Linter } from "eslint"; * Google Apps Script environment settings. */ const config: Linter.Config = { - files: [ "src/**/*.{js,mjs,cjs,ts,jsx,tsx}" ], + files: ["src/**/*.{js,mjs,cjs,ts,jsx,tsx}"], rules: { "no-restricted-globals": [ "error", diff --git a/config/eslint/lang-javascript.ts b/config/eslint/lang-javascript.ts index ecde0bf..2e9ecbc 100644 --- a/config/eslint/lang-javascript.ts +++ b/config/eslint/lang-javascript.ts @@ -7,7 +7,7 @@ import type { Linter } from "eslint"; * @see {@link https://eslint.org/docs/latest/rules/ ESLint rules} */ const config: Linter.Config = { - files: [ "**/*.{js,mjs,cjs}" ], + files: ["**/*.{js,mjs,cjs}"], ...js.configs.recommended }; diff --git a/config/eslint/lang-json.ts b/config/eslint/lang-json.ts index df6a6e3..7c0b281 100644 --- a/config/eslint/lang-json.ts +++ b/config/eslint/lang-json.ts @@ -8,18 +8,18 @@ import type { Linter } from "eslint"; */ const config: Array = [ { - files: [ "**/*.json" ], - ignores: [ "**/tsconfig.json", "**/tsconfig.*.json" ], + files: ["**/*.json"], + ignores: ["**/tsconfig.json", "**/tsconfig.*.json"], language: "json/json", ...json.configs.recommended }, { - files: [ "**/*.jsonc", "**/tsconfig.json", "**/tsconfig.*.json" ], + files: ["**/*.jsonc", "**/tsconfig.json", "**/tsconfig.*.json"], language: "json/jsonc", ...json.configs.recommended }, { - files: [ "**/*.json5" ], + files: ["**/*.json5"], language: "json/json5", ...json.configs.recommended } diff --git a/config/eslint/lang-markdown.ts b/config/eslint/lang-markdown.ts index 0c78527..f561c85 100644 --- a/config/eslint/lang-markdown.ts +++ b/config/eslint/lang-markdown.ts @@ -9,10 +9,10 @@ import type { Linter } from "eslint"; const config: Array = [ ...markdown.configs.recommended.map((config) => ({ ...config, - files: [ "**/*.md" ] + files: ["**/*.md"] })), { - files: [ "**/*.md" ], + files: ["**/*.md"], rules: { "markdown/no-missing-label-refs": "off" } diff --git a/config/eslint/lang-typescript.ts b/config/eslint/lang-typescript.ts index 6a13643..dd80475 100644 --- a/config/eslint/lang-typescript.ts +++ b/config/eslint/lang-typescript.ts @@ -8,7 +8,7 @@ import globals from "globals"; * @see https://typescript-eslint.io/rules/ */ const config: Linter.Config = { - files: [ "**/*.{js,mjs,cjs,ts,jsx,tsx}" ], + files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], languageOptions: { parser, globals: { diff --git a/config/eslint/overrides-tests.ts b/config/eslint/overrides-tests.ts index 00d0eb3..abf82cd 100644 --- a/config/eslint/overrides-tests.ts +++ b/config/eslint/overrides-tests.ts @@ -4,7 +4,7 @@ import type { Linter } from "eslint"; * Overrides for test files. */ const config: Linter.Config = { - files: [ "test/**/*.ts" ], + files: ["test/**/*.ts"], rules: { /** * Disallows usage of the `any` type. diff --git a/config/eslint/rules-jsdoc.ts b/config/eslint/rules-jsdoc.ts index 5de8d56..d5c3f7b 100644 --- a/config/eslint/rules-jsdoc.ts +++ b/config/eslint/rules-jsdoc.ts @@ -7,7 +7,7 @@ import jsdoc from "eslint-plugin-jsdoc"; * @see {@link https://www.npmjs.com/package/eslint-plugin-jsdoc eslint-plugin-jsdoc} */ const config: Linter.Config = { - files: [ "**/*.{js,mjs,cjs,ts,jsx,tsx}" ], + files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], plugins: { jsdoc }, diff --git a/config/eslint/rules-spacing.ts b/config/eslint/rules-spacing.ts index f5f1cf2..5815886 100644 --- a/config/eslint/rules-spacing.ts +++ b/config/eslint/rules-spacing.ts @@ -4,27 +4,27 @@ import type { Linter } from "eslint"; * Rules for managing spacing within brackets. */ const config: Linter.Config = { - files: [ "**/*.{js,mjs,cjs,ts,jsx,tsx}" ], + 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" ], + "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" ], + "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" ], + "computed-property-spacing": ["warn", "always"], /** * Requires a blank line between constants and blocks. @@ -34,13 +34,13 @@ const config: Linter.Config = { "warn", { blankLine: "always", - prev: [ "const", "let", "var" ], - next: [ "if", "for", "while", "switch", "try", "do", "block", "block-like" ] + 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" ] + prev: ["if", "for", "while", "switch", "try", "do", "block", "block-like"], + next: ["const", "let", "var"] } ] } diff --git a/config/vite/types/BuildOptions.ts b/config/vite/types/BuildOptions.ts index 598a71b..5ae673a 100644 --- a/config/vite/types/BuildOptions.ts +++ b/config/vite/types/BuildOptions.ts @@ -6,7 +6,6 @@ import { BuildPaths } from "./BuildPaths"; * Extends Vite's `ConfigEnv` with project-specific paths and flags. */ export interface BuildOptions extends ConfigEnv { - /** * Project paths. */ diff --git a/config/vite/types/BuildPaths.ts b/config/vite/types/BuildPaths.ts index b92b151..f122fca 100644 --- a/config/vite/types/BuildPaths.ts +++ b/config/vite/types/BuildPaths.ts @@ -2,7 +2,6 @@ * Directory paths used in the build process. */ export type BuildPaths = { - /** * Project root directory. */ diff --git a/config/vite/types/ResolveOptions.ts b/config/vite/types/ResolveOptions.ts index 1f27e36..569ba7b 100644 --- a/config/vite/types/ResolveOptions.ts +++ b/config/vite/types/ResolveOptions.ts @@ -4,7 +4,6 @@ import { AliasOptions, ResolveOptions as ViteResolveOptions } from "vite"; * Vite resolve options with optional aliases. */ export type ResolveOptions = ViteResolveOptions & { - /** * Module aliases. */ diff --git a/config/vite/utils/buildAliases.ts b/config/vite/utils/buildAliases.ts index 5b9a21b..2127757 100644 --- a/config/vite/utils/buildAliases.ts +++ b/config/vite/utils/buildAliases.ts @@ -16,7 +16,7 @@ export function buildAliases(rootDir: string): AliasOptions { .filter((dirent) => !dirent.name.startsWith(".")) .map((dirent) => { const name = dirent.name.replace(/\.ts$/, ""); - return [ name, join(rootDir, dirent.name) ]; + return [name, join(rootDir, dirent.name)]; }) ) : {}; diff --git a/package.json b/package.json index 7052d80..ce0d391 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,6 @@ "url": "https://buymeacoffee.com/MaksymStoianov" } ], - "peerDependencies": { - "typescript": "^5.9.3" - }, "devDependencies": { "@eslint/json": "^0.14.0", "@eslint/markdown": "^7.5.1", @@ -70,9 +67,13 @@ "eslint-plugin-jsdoc": "^62.5.2", "husky": "^9.1.7", "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", "reflect-metadata": "^0.2.2" diff --git a/src/controller/BootApplication.ts b/src/controller/BootApplication.ts index ad5fbea..6dd03b7 100644 --- a/src/controller/BootApplication.ts +++ b/src/controller/BootApplication.ts @@ -1,6 +1,13 @@ import { ApplicationConfig, InjectionToken, Newable } from "../domain/types"; import { AppsScriptEventType, RequestMethod } from "../domain/enums"; -import { EventDispatcher, RequestFactory, Resolver, ResponseBuilder, Router, RouterExplorer } from "../service"; +import { + EventDispatcher, + RequestFactory, + Resolver, + ResponseBuilder, + Router, + RouterExplorer +} from "../service"; export class BootApplication { private readonly _controllers = new Map(); diff --git a/src/controller/decorators/Controller.ts b/src/controller/decorators/Controller.ts index 05793e8..c5c163e 100644 --- a/src/controller/decorators/Controller.ts +++ b/src/controller/decorators/Controller.ts @@ -1,4 +1,8 @@ -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "../../domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + CONTROLLER_WATERMARK +} from "../../domain/constants"; /** * Controller options. diff --git a/src/domain/enums/AppsScriptEventType.ts b/src/domain/enums/AppsScriptEventType.ts index 30103e1..dd55884 100644 --- a/src/domain/enums/AppsScriptEventType.ts +++ b/src/domain/enums/AppsScriptEventType.ts @@ -4,7 +4,6 @@ * @see https://developers.google.com/apps-script/guides/triggers */ export enum AppsScriptEventType { - /** * Срабатывает при установке дополнения. * diff --git a/src/domain/enums/HeaderAcceptMimeType.ts b/src/domain/enums/HeaderAcceptMimeType.ts index 3b8eac2..4079c7d 100644 --- a/src/domain/enums/HeaderAcceptMimeType.ts +++ b/src/domain/enums/HeaderAcceptMimeType.ts @@ -4,7 +4,6 @@ * @see https://developers.google.com/apps-script/reference/content/mime-type */ export enum HeaderAcceptMimeType { - /** * Специальный тип для возврата JSON-строки напрямую (без TextOutput). * Используется для внутренних нужд Google Apps Script. diff --git a/src/repository/assignInjectMetadata.ts b/src/repository/assignInjectMetadata.ts index ec8e1a6..e9c9aac 100644 --- a/src/repository/assignInjectMetadata.ts +++ b/src/repository/assignInjectMetadata.ts @@ -18,6 +18,6 @@ export function assignInjectMetadata( return { ...existing, - [ `${type as string}:${index}` ]: { type, token, index } + [`${type as string}:${index}`]: { type, token, index } }; } diff --git a/src/repository/assignParamMetadata.ts b/src/repository/assignParamMetadata.ts index 682b222..c5d3f3b 100644 --- a/src/repository/assignParamMetadata.ts +++ b/src/repository/assignParamMetadata.ts @@ -18,6 +18,6 @@ export function assignParamMetadata( ): Record { return { ...existing, - [ `${type as string}:${index}` ]: { type, key, index } + [`${type as string}:${index}`]: { type, key, index } }; } diff --git a/src/service/EventDispatcher.ts b/src/service/EventDispatcher.ts index 7b18354..820150d 100644 --- a/src/service/EventDispatcher.ts +++ b/src/service/EventDispatcher.ts @@ -24,7 +24,7 @@ export class EventDispatcher { for (const propertyName of propertyNames) { if (propertyName === "constructor") continue; - const methodHandler = prototype[ propertyName ]; + const methodHandler = prototype[propertyName]; const eventMetadata = Reflect.getMetadata(APPSSCRIPT_EVENT_METADATA, methodHandler); @@ -35,7 +35,7 @@ export class EventDispatcher { const args = this.buildMethodParams(instance as object, propertyName, event); - const handler = (instance as Record)[ propertyName ] as ( + const handler = (instance as Record)[propertyName] as ( ...args: unknown[] ) => unknown; await handler.apply(instance, args); @@ -73,23 +73,23 @@ export class EventDispatcher { for (const param of metadata) { switch (param.type) { case ParamSource.EVENT: - args[ param.index ] = + args[param.index] = param.key && typeof event === "object" && event !== null - ? (event as Record)[ param.key ] + ? (event as Record)[param.key] : event; break; case ParamSource.INJECT: try { - const tokenToResolve = "token" in param ? param.token : designParamTypes[ param.index ]; + const tokenToResolve = "token" in param ? param.token : designParamTypes[param.index]; if (tokenToResolve) { - args[ param.index ] = this.resolver.resolve(tokenToResolve); + args[param.index] = this.resolver.resolve(tokenToResolve); } else { - args[ param.index ] = undefined; + args[param.index] = undefined; } } catch { - args[ param.index ] = undefined; + args[param.index] = undefined; } break; } @@ -118,7 +118,7 @@ export class EventDispatcher { return false; } - const ranges = Array.isArray(options.range) ? options.range : [ options.range ]; + const ranges = Array.isArray(options.range) ? options.range : [options.range]; // TODO: isRegExp return ranges.some((r: string | RegExp) => @@ -138,7 +138,7 @@ export class EventDispatcher { return false; } - const formIds = Array.isArray(options.formId) ? options.formId : [ options.formId ]; + const formIds = Array.isArray(options.formId) ? options.formId : [options.formId]; return formIds.some((id: string) => eventFormId === id); } @@ -155,7 +155,7 @@ export class EventDispatcher { const changeTypes = Array.isArray(options.changeType) ? options.changeType - : [ options.changeType ]; + : [options.changeType]; return changeTypes.some((type: unknown) => eventChangeType === type); } diff --git a/src/service/PathMatcher.ts b/src/service/PathMatcher.ts index c621f81..4cffbec 100644 --- a/src/service/PathMatcher.ts +++ b/src/service/PathMatcher.ts @@ -18,7 +18,7 @@ export class PathMatcher { if (part.startsWith("{") && part.endsWith("}")) { return true; } - return part === actParts[ i ]; + return part === actParts[i]; }); } @@ -37,7 +37,7 @@ export class PathMatcher { tplParts.forEach((part, i) => { if (part.startsWith("{") && part.endsWith("}")) { const paramName = part.slice(1, -1); - params[ paramName ] = actParts[ i ]; + params[paramName] = actParts[i]; } }); diff --git a/src/service/RequestFactory.ts b/src/service/RequestFactory.ts index 17c7c4f..d57a7ac 100644 --- a/src/service/RequestFactory.ts +++ b/src/service/RequestFactory.ts @@ -68,7 +68,7 @@ export class RequestFactory { } const contentType = - headers[ "Content-Type" ] || ("postData" in event ? event?.postData?.type : undefined) || ""; + headers["Content-Type"] || ("postData" in event ? event?.postData?.type : undefined) || ""; if (contentType.includes("application/json")) { try { diff --git a/src/service/Resolver.ts b/src/service/Resolver.ts index 6dec67c..602b4ab 100644 --- a/src/service/Resolver.ts +++ b/src/service/Resolver.ts @@ -45,9 +45,9 @@ export class Resolver { for (let i = 0; i < deps.length; i++) { const paramKey = `${ParamSource.INJECT}:${i}`; - const injectDefinition = explicitInjectTokens[ paramKey ]; + const injectDefinition = explicitInjectTokens[paramKey]; - const tokenToResolve = injectDefinition ? injectDefinition.token : designParamTypes[ i ]; + const tokenToResolve = injectDefinition ? injectDefinition.token : designParamTypes[i]; if (!tokenToResolve) { throw new Error( @@ -70,7 +70,7 @@ export class Resolver { ); } - deps[ i ] = isFunctionLike(tokenToResolve) + deps[i] = isFunctionLike(tokenToResolve) ? this.resolve(tokenToResolve as Newable) : this._providers.get(tokenToResolve); } diff --git a/src/service/ResponseBuilder.ts b/src/service/ResponseBuilder.ts index 86258ba..3a97ad1 100644 --- a/src/service/ResponseBuilder.ts +++ b/src/service/ResponseBuilder.ts @@ -19,14 +19,14 @@ export class ResponseBuilder { ): 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"; + return entry ? entry[0] : "UNKNOWN_STATUS"; })(); const ok = resolvedStatus >= 200 && resolvedStatus < 300; @@ -53,7 +53,7 @@ export class ResponseBuilder { ): string | GoogleAppsScript.Content.TextOutput | GoogleAppsScript.HTML.HtmlOutput { const mimeType = (request.headers?.Accept as HeaderAcceptMimeType) || HeaderAcceptMimeType.HTML; - response.headers[ "Content-Type" ] = mimeType; + response.headers["Content-Type"] = mimeType; const isApi = request.url.pathname?.startsWith("/api/") || false; diff --git a/src/service/Router.ts b/src/service/Router.ts index eab3163..f490eb5 100644 --- a/src/service/Router.ts +++ b/src/service/Router.ts @@ -62,7 +62,7 @@ export class Router { const args = this.buildMethodParams(controllerInstance as object, route.handler, ctx); try { - const handler = (controllerInstance as Record)[ route.handler ] as ( + const handler = (controllerInstance as Record)[route.handler] as ( ...args: unknown[] ) => unknown; const result = await handler.apply(controllerInstance, args); @@ -111,31 +111,31 @@ export class Router { for (const param of metadata) { switch (param.type) { case ParamSource.PARAM: - args[ param.index ] = param.key ? (ctx.params ?? {})[ param.key ] : ctx.params; + 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; + args[param.index] = param.key ? (ctx.query ?? {})[param.key] : ctx.query; break; case ParamSource.BODY: - args[ param.index ] = + args[param.index] = param.key && ctx.body && isObject(ctx.body) - ? (ctx.body as unknown as Record)[ param.key ] + ? (ctx.body as unknown as Record)[param.key] : ctx.body; break; case ParamSource.EVENT: - args[ param.index ] = + args[param.index] = param.key && isObject(ctx.event) - ? (ctx.event as unknown as Record)[ param.key ] + ? (ctx.event as unknown as Record)[param.key] : ctx.event; break; case ParamSource.REQUEST: - args[ param.index ] = + args[param.index] = param.key && isObject(ctx.request) - ? (ctx.request as unknown as Record)[ param.key ] + ? (ctx.request as unknown as Record)[param.key] : ctx.request; break; @@ -144,30 +144,30 @@ export class Router { const headerKey = Object.keys(ctx.headers).find( (k) => k.toLowerCase() === param.key!.toLowerCase() ); - args[ param.index ] = headerKey ? ctx.headers[ headerKey ] : undefined; + args[param.index] = headerKey ? ctx.headers[headerKey] : undefined; } else { - args[ param.index ] = ctx.headers; + args[param.index] = ctx.headers; } break; case ParamSource.RESPONSE: - args[ param.index ] = + args[param.index] = param.key && isObject(ctx.response) - ? (ctx.response as unknown as Record)[ param.key ] + ? (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 ]; + const tokenToResolve = "token" in param ? param.token : designParamTypes[param.index]; if (tokenToResolve) { - args[ param.index ] = this._resolver.resolve(tokenToResolve); + args[param.index] = this._resolver.resolve(tokenToResolve); } else { - args[ param.index ] = undefined; + args[param.index] = undefined; } } catch { - args[ param.index ] = undefined; + args[param.index] = undefined; } break; } diff --git a/src/service/RouterExplorer.ts b/src/service/RouterExplorer.ts index b2b0976..dbb46b9 100644 --- a/src/service/RouterExplorer.ts +++ b/src/service/RouterExplorer.ts @@ -1,5 +1,10 @@ import { normalize } from "apps-script-utils"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, METHOD_METADATA, PATH_METADATA } from "../domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + METHOD_METADATA, + PATH_METADATA +} from "../domain/constants"; import { Newable, RouteMetadata } from "../domain/types"; export class RouterExplorer { @@ -29,7 +34,7 @@ export class RouterExplorer { continue; } - const methodHandler = prototype[ propertyName ]; + const methodHandler = prototype[propertyName]; const routePath = Reflect.getMetadata(PATH_METADATA, methodHandler); diff --git a/src/shared/utils/createResponse.ts b/src/shared/utils/createResponse.ts index 8df9f1e..a3d4fcb 100644 --- a/src/shared/utils/createResponse.ts +++ b/src/shared/utils/createResponse.ts @@ -22,18 +22,14 @@ 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"; + return entry ? entry[0] : "UNKNOWN_STATUS"; })(); const ok = resolvedStatus >= 200 && resolvedStatus < 300; diff --git a/src/shared/utils/extractPathParams.ts b/src/shared/utils/extractPathParams.ts index f53b16f..3568f8d 100644 --- a/src/shared/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 = {}; @@ -17,7 +14,7 @@ export function extractPathParams( tplParts.forEach((part, i) => { if (part.startsWith("{") && part.endsWith("}")) { const paramName = part.slice(1, -1); - params[ paramName ] = actParts[ i ]; + params[paramName] = actParts[i]; } }); diff --git a/src/shared/utils/pathMatch.ts b/src/shared/utils/pathMatch.ts index 39ee00f..e99b2c1 100644 --- a/src/shared/utils/pathMatch.ts +++ b/src/shared/utils/pathMatch.ts @@ -17,6 +17,6 @@ export function pathMatch(template: string, actual: string): boolean { if (part.startsWith("{") && part.endsWith("}")) { return true; } - return part === actParts[ i ]; + return part === actParts[i]; }); } diff --git a/src/shared/utils/wrapResponse.ts b/src/shared/utils/wrapResponse.ts index 1144309..43ef5a4 100644 --- a/src/shared/utils/wrapResponse.ts +++ b/src/shared/utils/wrapResponse.ts @@ -13,15 +13,10 @@ import { HttpRequest, HttpResponse } from "../../domain/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; + response.headers["Content-Type"] = mimeType; const isApi = request.url.pathname?.startsWith("/api/") || false; const result = JSON.stringify(isApi ? response : response.body); @@ -34,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/test/integration/BootApplication/BootApplication.negative.integration.test.ts b/test/integration/BootApplication/BootApplication.negative.integration.test.ts index c0a7dee..cba5a7a 100644 --- a/test/integration/BootApplication/BootApplication.negative.integration.test.ts +++ b/test/integration/BootApplication/BootApplication.negative.integration.test.ts @@ -14,7 +14,7 @@ class UserController { describe("Integration: BootApplication: Negative", () => { const app = BootApplicationFactory.create({ - controllers: [ UserController ] + controllers: [UserController] }); (global as unknown as Record).HtmlService = { @@ -31,7 +31,7 @@ describe("Integration: BootApplication: Negative", () => { await app.doGet(event); expect(global.HtmlService.createHtmlOutput).toHaveBeenCalled(); - const callArgs = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls[ 0 ][ 0 ]; + 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 index 847a353..22d2bda 100644 --- a/test/integration/BootApplication/BootApplication.positive.integration.test.ts +++ b/test/integration/BootApplication/BootApplication.positive.integration.test.ts @@ -20,7 +20,7 @@ class UserController { describe("Integration: BootApplication: Positive", () => { const app = BootApplicationFactory.create({ - controllers: [ UserController ] + controllers: [UserController] }); // Mock ContentService and HtmlService @@ -41,7 +41,7 @@ describe("Integration: BootApplication: Positive", () => { it("should handle GET request with path and query params", async () => { const event = { parameter: { fields: "name,email" }, - parameters: { fields: [ "name,email" ] }, + parameters: { fields: ["name,email"] }, contextPath: "", contentLength: -1, queryString: "fields=name,email", @@ -51,9 +51,9 @@ describe("Integration: BootApplication: Positive", () => { await app.doGet(event); expect(global.HtmlService.createHtmlOutput).toHaveBeenCalled(); - const callArgs = vi.mocked(global.HtmlService.createHtmlOutput).mock.calls[ 0 ][ 0 ]; + 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" }); + expect(responseBody).toEqual({ id: "123", fields: ["name,email"], method: "GET" }); }); it("should handle POST request with body", async () => { @@ -70,7 +70,7 @@ describe("Integration: BootApplication: Positive", () => { 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 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 index 2f5b860..1fc374d 100644 --- a/test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts +++ b/test/unit/controller/BootApplication/BootApplication.positive.unit.test.ts @@ -13,7 +13,7 @@ describe("BootApplication: Positive", () => { const app = new BootApplication({ controllers: [], - providers: [ ClassProvider, valueProvider, classProvider, factoryProvider, existingProvider ] + providers: [ClassProvider, valueProvider, classProvider, factoryProvider, existingProvider] }); const providers = (app as unknown as { _providers: Map })._providers; diff --git a/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts b/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts index ef72d9f..d6ed849 100644 --- a/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts +++ b/test/unit/controller/decorators/Controller/Controller.positive.unit.test.ts @@ -1,6 +1,10 @@ import "reflect-metadata"; import { describe, expect, it } from "vitest"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "src/domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + CONTROLLER_WATERMARK +} from "src/domain/constants"; import { Controller } from "src/controller/decorators"; describe("@Controller: Positive", () => { diff --git a/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts b/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts index b163355..8ffe1fb 100644 --- a/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts +++ b/test/unit/controller/decorators/HttpController/HttpController.negative.unit.test.ts @@ -1,6 +1,10 @@ import "reflect-metadata"; import { describe, expect, it } from "vitest"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "src/domain/constants"; +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", () => { diff --git a/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts b/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts index 63442fe..dc368f6 100644 --- a/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts +++ b/test/unit/controller/decorators/HttpController/HttpController.positive.unit.test.ts @@ -1,6 +1,10 @@ import "reflect-metadata"; import { describe, expect, it } from "vitest"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "src/domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + CONTROLLER_WATERMARK +} from "src/domain/constants"; import { HttpController, RestController } from "src/controller/decorators"; describe("HttpController Decorator: Positive", () => { diff --git a/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts index df0ec13..86148d2 100644 --- a/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts +++ b/test/unit/controller/decorators/Inject/Inject.boundary.unit.test.ts @@ -14,8 +14,8 @@ class StringTokenController { 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" } ] + controllers: [StringTokenController], + providers: [{ provide: STRING_TOKEN, useValue: "token_value" }] }); const instance = (app as unknown as { _resolver: Resolver })._resolver.resolve( diff --git a/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts index d641736..26844ea 100644 --- a/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts +++ b/test/unit/controller/decorators/Inject/Inject.negative.unit.test.ts @@ -15,7 +15,7 @@ class FaultyController { describe("Inject Decorator: Negative", () => { it("should throw error when injecting an unregistered provider", () => { const app = BootApplicationFactory.create({ - controllers: [ FaultyController ], + controllers: [FaultyController], providers: [] }); diff --git a/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts b/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts index a43d41c..3c68ff5 100644 --- a/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts +++ b/test/unit/controller/decorators/Inject/Inject.positive.unit.test.ts @@ -35,8 +35,8 @@ class TestController { describe("Inject Decorator: Positive", () => { it("should correctly inject dependencies using constructor and @Inject", () => { const app = BootApplicationFactory.create({ - controllers: [ TestController ], - providers: [ ServiceA, ServiceB ] + controllers: [TestController], + providers: [ServiceA, ServiceB] }); const instance = (app as unknown as { _resolver: Resolver })._resolver.resolve(TestController); diff --git a/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts b/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts index 40ed011..5b19411 100644 --- a/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts +++ b/test/unit/controller/decorators/RestController/RestController.boundary.unit.test.ts @@ -1,6 +1,10 @@ import "reflect-metadata"; import { describe, expect, it } from "vitest"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "src/domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + CONTROLLER_WATERMARK +} from "src/domain/constants"; import { RestController } from "src/controller/decorators"; describe("@RestController: Boundary", () => { diff --git a/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts b/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts index 18e48c8..6077da2 100644 --- a/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts +++ b/test/unit/controller/decorators/RestController/RestController.positive.unit.test.ts @@ -1,6 +1,10 @@ import "reflect-metadata"; import { describe, expect, it } from "vitest"; -import { CONTROLLER_OPTIONS_METADATA, CONTROLLER_TYPE_METADATA, CONTROLLER_WATERMARK } from "src/domain/constants"; +import { + CONTROLLER_OPTIONS_METADATA, + CONTROLLER_TYPE_METADATA, + CONTROLLER_WATERMARK +} from "src/domain/constants"; import { RestController } from "src/controller/decorators"; describe("@RestController: Positive", () => { 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 index b08bb0d..3e8f66c 100644 --- a/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts +++ b/test/unit/controller/decorators/params/Integration/Integration.positive.unit.test.ts @@ -1,7 +1,15 @@ 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 { + Body, + Event, + Headers, + Param, + Query, + Request, + Response +} from "src/controller/decorators/params"; import { ParamSource } from "src/domain/enums"; function getParameterMetadata( diff --git a/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts b/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts index a11fcf7..ae39f8e 100644 --- a/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts +++ b/test/unit/repository/createParamDecorator/createParamDecorator.boundary.unit.test.ts @@ -15,8 +15,8 @@ describe("createParamDecorator: boundary", () => { 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 } + [`${ParamSource.QUERY}:0`]: { type: ParamSource.QUERY, key: "id", index: 0 }, + [`${ParamSource.BODY}:1`]: { type: ParamSource.BODY, key: undefined, index: 1 } }); }); @@ -30,7 +30,7 @@ describe("createParamDecorator: boundary", () => { const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method"); expect(metadata).toEqual({ - [ `${ParamSource.HEADERS}:0` ]: { type: ParamSource.HEADERS, key: undefined, index: 0 } + [`${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 index 54f0787..825b952 100644 --- a/test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts +++ b/test/unit/repository/createParamDecorator/createParamDecorator.positive.unit.test.ts @@ -17,7 +17,7 @@ describe("createParamDecorator: positive", () => { const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test.prototype, "method"); expect(metadata).toEqual({ - [ `${type}:0` ]: { type, key, index: 0 } + [`${type}:0`]: { type, key, index: 0 } }); }); @@ -33,7 +33,7 @@ describe("createParamDecorator: positive", () => { const metadata = Reflect.getMetadata(PARAM_DEFINITIONS_METADATA, Test); expect(metadata).toEqual({ - [ `${type}:0` ]: { type, key, index: 0 } + [`${type}:0`]: { type, key, index: 0 } }); }); }); diff --git a/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts b/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts index bf3774e..1b47741 100644 --- a/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts +++ b/test/unit/repository/getInjectionTokens/getInjectionTokens.positive.unit.test.ts @@ -8,7 +8,7 @@ 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 } + [`${ParamSource.INJECT}:0`]: { type: ParamSource.INJECT, token: "ServiceA", index: 0 } }; Reflect.defineMetadata(INJECT_TOKENS_METADATA, tokens, Test); @@ -21,7 +21,7 @@ describe("getInjectionTokens: positive", () => { method() {} } const tokens = { - [ `${ParamSource.INJECT}:0` ]: { type: ParamSource.INJECT, token: "ServiceB", index: 0 } + [`${ParamSource.INJECT}:0`]: { type: ParamSource.INJECT, token: "ServiceB", index: 0 } }; Reflect.defineMetadata(INJECT_TOKENS_METADATA, tokens, Test.prototype, "method"); diff --git a/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts index be74462..bede5a0 100644 --- a/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts +++ b/test/unit/service/EventDispatcher/EventDispatcher.boundary.unit.test.ts @@ -19,8 +19,8 @@ describe("EventDispatcher: Boundary", () => { beforeEach(() => { controller = new TestEventController(); app = BootApplicationFactory.create({ - controllers: [ TestEventController ], - providers: [ { provide: TestEventController, useValue: controller } ] + controllers: [TestEventController], + providers: [{ provide: TestEventController, useValue: controller }] }); }); diff --git a/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts index bf2f091..c786987 100644 --- a/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts +++ b/test/unit/service/EventDispatcher/EventDispatcher.extra.unit.test.ts @@ -26,7 +26,7 @@ describe("EventDispatcher: Extra", () => { } const instance = new TestController(); vi.mocked(resolver.resolve).mockReturnValue(instance); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: { getA1Notation: () => "A10" } }); expect(instance.called).toBe(true); @@ -46,7 +46,7 @@ describe("EventDispatcher: Extra", () => { } const instance = new TestController(); vi.mocked(resolver.resolve).mockReturnValue(instance); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: {} }); // Missing getA1Notation expect(instance.called).toBe(false); @@ -62,7 +62,7 @@ describe("EventDispatcher: Extra", () => { } const instance = new TestController(); vi.mocked(resolver.resolve).mockReturnValue(instance); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, { source: { getId: () => "FORM_1" } @@ -86,7 +86,7 @@ describe("EventDispatcher: Extra", () => { } const instance = new TestController(); vi.mocked(resolver.resolve).mockReturnValue(instance); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.FORM_SUBMIT, { source: {} }); // Missing getId expect(instance.called).toBe(false); @@ -102,7 +102,7 @@ describe("EventDispatcher: Extra", () => { } const instance = new TestController(); vi.mocked(resolver.resolve).mockReturnValue(instance); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.CHANGE, {}); // Missing changeType expect(instance.called).toBe(false); @@ -125,7 +125,7 @@ describe("EventDispatcher: Extra", () => { return null; }); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: { getA1Notation: () => "A1" } @@ -147,7 +147,7 @@ describe("EventDispatcher: Extra", () => { throw new Error("Resolve failed"); }); - dispatcher = new EventDispatcher(resolver, new Map([ [ TestController, null ] ])); + dispatcher = new EventDispatcher(resolver, new Map([[TestController, null]])); await dispatcher.dispatch(AppsScriptEventType.EDIT, { range: { getA1Notation: () => "A1" } diff --git a/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts index 03f9b13..a63b59a 100644 --- a/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts +++ b/test/unit/service/EventDispatcher/EventDispatcher.negative.unit.test.ts @@ -25,8 +25,8 @@ describe("EventDispatcher: Negative", () => { beforeEach(() => { controller = new TestEventController(); app = BootApplicationFactory.create({ - controllers: [ TestEventController ], - providers: [ { provide: TestEventController, useValue: controller } ] + controllers: [TestEventController], + providers: [{ provide: TestEventController, useValue: controller }] }); }); diff --git a/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts b/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts index de63ff8..f598af8 100644 --- a/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts +++ b/test/unit/service/EventDispatcher/EventDispatcher.positive.unit.test.ts @@ -34,8 +34,8 @@ describe("EventDispatcher: Positive", () => { beforeEach(() => { controller = new TestEventController(); app = BootApplicationFactory.create({ - controllers: [ TestEventController ], - providers: [ { provide: TestEventController, useValue: controller } ] + controllers: [TestEventController], + providers: [{ provide: TestEventController, useValue: controller }] }); }); diff --git a/test/unit/service/Resolver/Resolver.extra.unit.test.ts b/test/unit/service/Resolver/Resolver.extra.unit.test.ts index 08c62d6..b84dd0b 100644 --- a/test/unit/service/Resolver/Resolver.extra.unit.test.ts +++ b/test/unit/service/Resolver/Resolver.extra.unit.test.ts @@ -15,14 +15,14 @@ describe("Resolver: Extra", () => { constructor(_dep: unknown) {} } // Simulate invalid metadata - Reflect.defineMetadata(PARAMTYPES_METADATA, [ undefined ], Target); + 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); + Reflect.defineMetadata(PARAMTYPES_METADATA, ["not-a-function"], Target); expect(() => resolver.resolve(Target)).toThrow("Invalid injection token"); }); diff --git a/test/unit/service/Services.extra.unit.test.ts b/test/unit/service/Services.extra.unit.test.ts index 8f13451..7ad7016 100644 --- a/test/unit/service/Services.extra.unit.test.ts +++ b/test/unit/service/Services.extra.unit.test.ts @@ -36,10 +36,10 @@ describe("Services: Extra Coverage (RequestFactory & RouterExplorer)", () => { } const explorer = new RouterExplorer(); - const routes = explorer.explore(new Map([ [ TestController, null ] ])); + const routes = explorer.explore(new Map([[TestController, null]])); expect(routes.length).toBe(1); - expect(routes[ 0 ].path).toBe("/test"); + expect(routes[0].path).toBe("/test"); }); }); });